У меня огромный файл, который я должен разбирать построчно. Скорость имеет значение.
Пример строки:
Token-1 Here-is-the-Next-Token Last-Token-on-Line ^ ^ Current Position Position after GetToken
GetToken вызывается, возвращает «Here-is-the-Next-Token» и устанавливает CurrentPosition на позицию последнего символа токена, чтобы он был готов к следующему вызову GetToken. Жетоны разделяются одним или несколькими пробелами.
Предположим, что файл уже находится в StringList в памяти. Легко умещается в памяти, скажем 200 Мб.
Меня беспокоит только время выполнения парсинга. Какой код обеспечит самое быстрое выполнение в Delphi (Паскаль)?





Сворачивание собственного - это, безусловно, самый быстрый способ. Для получения дополнительной информации по этой теме вы можете увидеть Исходный код Synedit, который содержит лексеры (называемые маркерами в контексте проекта) для любого языка на рынке. Я предлагаю вам взять один из этих лексеров за основу и изменить его для вашего собственного использования.
Я сделал лексический анализатор на основе движка состояний (DFA). Работает со столом и работает довольно быстро. Но возможны более быстрые варианты.
Это тоже зависит от языка. У простого языка может быть умный алгоритм.
Таблица представляет собой массив записей, каждая из которых содержит 2 символа и 1 целое число. Для каждого токена лексер проходит по таблице, начиная с позиции 0:
state := 0;
result := tkNoToken;
while (result = tkNoToken) do begin
if table[state].c1 > table[state].c2 then
result := table[state].value
else if (table[state].c1 <= c) and (c <= table[state].c2) then begin
c := GetNextChar();
state := table[state].value;
end else
Inc(state);
end;
Это просто и работает как шарм.
Я думаю, что самым большим узким местом всегда будет загрузка файла в память. Если он у вас есть в памяти (очевидно, не все сразу, но на вашем месте я бы работал с буферами), фактический синтаксический анализ не должен иметь значения.
Вообще-то нет. Потребовалось 0,04 секунды для простого чтения файла размером 25 МБ в буфер и 0,17 секунды для его кодирования (для преобразования ASCII в Unicode). Затем потребовалось 4,5 секунды, чтобы прочитать 25 миллионов символов и разобрать части строки. Так что мне нужна скорость в парсере.
Самым быстрым способом написать кода было бы, вероятно, создать TStringList и назначить каждую строку в текстовом файле свойству CommaText. По умолчанию пробел является разделителем, поэтому вы получите один элемент StringList для каждого токена.
MyStringList.CommaText := s;
for i := 0 to MyStringList.Count - 1 do
begin
// process each token here
end;
Однако вы, вероятно, получите лучшую производительность, если проанализируете каждую строку самостоятельно.
Извиняюсь. Я не имел в виду самый быстрый способ «написать» код. Мне действительно нужен был самый быстрый код. Сейчас я редактирую вопрос, чтобы сделать это очевидным.
Вот убогая реализация очень простого лексера. Это может дать вам представление.
Обратите внимание на ограничения этого примера - без буферизации, без Unicode (это отрывок из проекта Delphi 7). Они, вероятно, понадобятся вам в серьезной реализации.
{ Implements a simpe lexer class. }
unit Simplelexer;
interface
uses Classes, Sysutils, Types, dialogs;
type
ESimpleLexerFinished = class(Exception) end;
TProcTableProc = procedure of object;
// A very simple lexer that can handle numbers, words, symbols - no comment handling
TSimpleLexer = class(TObject)
private
FLineNo: Integer;
Run: Integer;
fOffset: Integer;
fRunOffset: Integer; // helper for fOffset
fTokenPos: Integer;
pSource: PChar;
fProcTable: array[#0..#255] of TProcTableProc;
fUseSimpleStrings: Boolean;
fIgnoreSpaces: Boolean;
procedure MakeMethodTables;
procedure IdentProc;
procedure NewLineProc;
procedure NullProc;
procedure NumberProc;
procedure SpaceProc;
procedure SymbolProc;
procedure UnknownProc;
public
constructor Create;
destructor Destroy; override;
procedure Feed(const S: string);
procedure Next;
function GetToken: string;
function GetLineNo: Integer;
function GetOffset: Integer;
property IgnoreSpaces: boolean read fIgnoreSpaces write fIgnoreSpaces;
property UseSimpleStrings: boolean read fUseSimpleStrings write fUseSimpleStrings;
end;
implementation
{ TSimpleLexer }
constructor TSimpleLexer.Create;
begin
makeMethodTables;
fUseSimpleStrings := false;
fIgnoreSpaces := false;
end;
destructor TSimpleLexer.Destroy;
begin
inherited;
end;
procedure TSimpleLexer.Feed(const S: string);
begin
Run := 0;
FLineNo := 1;
FOffset := 1;
pSource := PChar(S);
end;
procedure TSimpleLexer.Next;
begin
fTokenPos := Run;
foffset := Run - frunOffset + 1;
fProcTable[pSource[Run]];
end;
function TSimpleLexer.GetToken: string;
begin
SetString(Result, (pSource + fTokenPos), Run - fTokenPos);
end;
function TSimpleLexer.GetLineNo: Integer;
begin
Result := FLineNo;
end;
function TSimpleLexer.GetOffset: Integer;
begin
Result := foffset;
end;
procedure TSimpleLexer.MakeMethodTables;
var
I: Char;
begin
for I := #0 to #255 do
case I of
'@', '&', '}', '{', ':', ',', ']', '[', '*',
'^', ')', '(', ';', '/', '=', '-', '+', '#', '>', '<', '$',
'.', '"', #39:
fProcTable[I] := SymbolProc;
#13, #10: fProcTable[I] := NewLineProc;
'A'..'Z', 'a'..'z', '_': fProcTable[I] := IdentProc;
#0: fProcTable[I] := NullProc;
'0'..'9': fProcTable[I] := NumberProc;
#1..#9, #11, #12, #14..#32: fProcTable[I] := SpaceProc;
else
fProcTable[I] := UnknownProc;
end;
end;
procedure TSimpleLexer.UnknownProc;
begin
inc(run);
end;
procedure TSimpleLexer.SymbolProc;
begin
if fUseSimpleStrings then
begin
if pSource[run] = '"' then
begin
Inc(run);
while pSource[run] <> '"' do
begin
Inc(run);
if pSource[run] = #0 then
begin
NullProc;
end;
end;
end;
Inc(run);
end
else
inc(run);
end;
procedure TSimpleLexer.IdentProc;
begin
while pSource[Run] in ['_', 'A'..'Z', 'a'..'z', '0'..'9'] do
Inc(run);
end;
procedure TSimpleLexer.NumberProc;
begin
while pSource[run] in ['0'..'9'] do
inc(run);
end;
procedure TSimpleLexer.SpaceProc;
begin
while pSource[run] in [#1..#9, #11, #12, #14..#32] do
inc(run);
if fIgnoreSpaces then Next;
end;
procedure TSimpleLexer.NewLineProc;
begin
inc(FLineNo);
inc(run);
case pSource[run - 1] of
#13:
if pSource[run] = #10 then inc(run);
end;
foffset := 1;
fRunOffset := run;
end;
procedure TSimpleLexer.NullProc;
begin
raise ESimpleLexerFinished.Create('');
end;
end.
Использование PChar напрямую, а не индексация, и копирование местоположения PChar в локальный, чтобы ему можно было выделить регистр, - это пара простых оптимизаций, которые вы можете применить к своему подходу. Кроме того, определение типа токена может быть эффективно выполнено с помощью оператора case, а не table + func.
Напрашивается другой вопрос - насколько большой? Дайте нам подсказку, например, # строк или # или Мб (Гб)? Тогда мы узнаем, умещается ли он в памяти, должен ли он быть дисковым и т. д.
На первом этапе я бы использовал свой WordList (S: String; AList: TStringlist);
тогда вы можете получить доступ к каждому токену как Alist [n] ... или отсортировать их или что-то еще.
Нет. Легко умещается в памяти. Скажем, 200 МБ. Предположим, он уже находится в StringList. Отредактирую вопрос и добавлю уточнить.
Вот пример лексера, который должен быть довольно эффективным, но предполагает, что все исходные данные находятся в одной строке. Переделать его для работы с буферами довольно сложно из-за очень длинных токенов.
type
TLexer = class
private
FData: string;
FTokenStart: PChar;
FCurrPos: PChar;
function GetCurrentToken: string;
public
constructor Create(const AData: string);
function GetNextToken: Boolean;
property CurrentToken: string read GetCurrentToken;
end;
{ TLexer }
constructor TLexer.Create(const AData: string);
begin
FData := AData;
FCurrPos := PChar(FData);
end;
function TLexer.GetCurrentToken: string;
begin
SetString(Result, FTokenStart, FCurrPos - FTokenStart);
end;
function TLexer.GetNextToken: Boolean;
var
cp: PChar;
begin
cp := FCurrPos; // copy to local to permit register allocation
// skip whitespace; this test could be converted to an unsigned int
// subtraction and compare for only a single branch
while (cp^ > #0) and (cp^ <= #32) do
Inc(cp);
// using null terminater for end of file
Result := cp^ <> #0;
if Result then
begin
FTokenStart := cp;
Inc(cp);
while cp^ > #32 do
Inc(cp);
end;
FCurrPos := cp;
end;
После анализа скорость всегда будет зависеть от того, что вы делаете. Лексический синтаксический анализатор на сегодняшний день является самым быстрым методом преобразования в токены из текстового потока независимо от размера. TParser в модуле классов - отличное место для начала.
Лично мне уже давно не нужно было писать синтаксический анализатор, но еще один более устаревший, но проверенный и верный метод - использовать LEX / YACC для построения грамматики, а затем преобразовать грамматику в код, который вы можете использовать для выполнения своей обработки. DYacc - это версия Delphi ... не уверен, компилируется она до сих пор или нет, но стоит взглянуть, если вы хотите делать что-то по-старому. книга дракона здесь будет большим подспорьем, если вы сможете найти копию.
Если скорость важна, ответ - это специальный код. Ознакомьтесь с Windows API, который отобразит ваш файл в память. Затем вы можете просто использовать указатель на следующий символ, чтобы делать свои токены, проходя по мере необходимости.
Это мой код для сопоставления:
procedure TMyReader.InitialiseMapping(szFilename : string);
var
// nError : DWORD;
bGood : boolean;
begin
bGood := False;
m_hFile := CreateFile(PChar(szFilename), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
if m_hFile <> INVALID_HANDLE_VALUE then
begin
m_hMap := CreateFileMapping(m_hFile, nil, PAGE_READONLY, 0, 0, nil);
if m_hMap <> 0 then
begin
m_pMemory := MapViewOfFile(m_hMap, FILE_MAP_READ, 0, 0, 0);
if m_pMemory <> nil then
begin
htlArray := Pointer(Integer(m_pMemory) + m_dwDataPosition);
bGood := True;
end
else
begin
// nError := GetLastError;
end;
end;
end;
if not bGood then
raise Exception.Create('Unable to map token file into memory');
end;
Я прочитал свой файл с помощью TFileStream.Create, Read, TEncoding.GetBufferEncoding и Encoding.GetString. Это очень быстро загружает StringList. Я понимаю, что файлы с отображением в память часто работают быстрее при произвольном доступе, но никогда при последовательном доступе. Также мне все равно придется делать кодирование.
Переходы между состояниями DFA могут быть реализованы в виде таблицы, да, но другой способ реализовать их - неявно через счетчик программ. Обычно он оказывается более четким и эффективным, чем DFA, который больше подходит для автоматической генерации.