Каков самый быстрый способ разобрать строку в Delphi?

У меня огромный файл, который я должен разбирать построчно. Скорость имеет значение.

Пример строки:

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 (Паскаль)?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
0
10 712
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Сворачивание собственного - это, безусловно, самый быстрый способ. Для получения дополнительной информации по этой теме вы можете увидеть Исходный код 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;

Это просто и работает как шарм.

Переходы между состояниями DFA могут быть реализованы в виде таблицы, да, но другой способ реализовать их - неявно через счетчик программ. Обычно он оказывается более четким и эффективным, чем DFA, который больше подходит для автоматической генерации.

Barry Kelly 13.11.2008 23:38

Я думаю, что самым большим узким местом всегда будет загрузка файла в память. Если он у вас есть в памяти (очевидно, не все сразу, но на вашем месте я бы работал с буферами), фактический синтаксический анализ не должен иметь значения.

Вообще-то нет. Потребовалось 0,04 секунды для простого чтения файла размером 25 МБ в буфер и 0,17 секунды для его кодирования (для преобразования ASCII в Unicode). Затем потребовалось 4,5 секунды, чтобы прочитать 25 миллионов символов и разобрать части строки. Так что мне нужна скорость в парсере.

lkessler 18.11.2008 09:21

Самым быстрым способом написать кода было бы, вероятно, создать TStringList и назначить каждую строку в текстовом файле свойству CommaText. По умолчанию пробел является разделителем, поэтому вы получите один элемент StringList для каждого токена.

MyStringList.CommaText := s;
for i := 0 to MyStringList.Count - 1 do
begin
  // process each token here
end;

Однако вы, вероятно, получите лучшую производительность, если проанализируете каждую строку самостоятельно.

Извиняюсь. Я не имел в виду самый быстрый способ «написать» код. Мне действительно нужен был самый быстрый код. Сейчас я редактирую вопрос, чтобы сделать это очевидным.

lkessler 13.11.2008 22:12

Вот убогая реализация очень простого лексера. Это может дать вам представление.

Обратите внимание на ограничения этого примера - без буферизации, без 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.

Barry Kelly 13.11.2008 23:40

Напрашивается другой вопрос - насколько большой? Дайте нам подсказку, например, # строк или # или Мб (Гб)? Тогда мы узнаем, умещается ли он в памяти, должен ли он быть дисковым и т. д.

На первом этапе я бы использовал свой WordList (S: String; AList: TStringlist);

тогда вы можете получить доступ к каждому токену как Alist [n] ... или отсортировать их или что-то еще.

Нет. Легко умещается в памяти. Скажем, 200 МБ. Предположим, он уже находится в StringList. Отредактирую вопрос и добавлю уточнить.

lkessler 13.11.2008 22:50
Ответ принят как подходящий
  • Используйте приращение PChar для увеличения скорости обработки
  • Если некоторые токены не нужны, копируйте данные токенов только по запросу.
  • Скопируйте PChar в локальную переменную при фактическом сканировании символов
  • Храните исходные данные в одном буфере, если вы не должны обрабатывать строку за строкой, и даже тогда рассмотрите возможность обработки обработки строки как отдельного токена в распознавателе лексера.
  • Рассмотрите возможность обработки буфера массива байтов, полученного прямо из файла, если вы точно знаете кодировку; при использовании Delphi 2009 используйте PAnsiChar вместо PChar, если, конечно, вы не знаете, что кодировка - UTF16-LE.
  • Если вы знаете, что единственным пробелом будет # 32 (пространство ASCII) или аналогичный ограниченный набор символов, могут быть некоторые хитрые приемы манипулирования битами, которые позволят вам обрабатывать 4 байта за раз, используя целочисленное сканирование. Я бы не ожидал здесь больших выигрышей, а код будет чистым, как грязь.

Вот пример лексера, который должен быть довольно эффективным, но предполагает, что все исходные данные находятся в одной строке. Переделать его для работы с буферами довольно сложно из-за очень длинных токенов.

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. Я понимаю, что файлы с отображением в память часто работают быстрее при произвольном доступе, но никогда при последовательном доступе. Также мне все равно придется делать кодирование.

lkessler 18.11.2008 04:33

Другие вопросы по теме