【问题标题】:What is the fastest way to Parse a line in Delphi?在 Delphi 中解析一行的最快方法是什么?
【发布时间】:2010-09-22 05:08:52
【问题描述】:

我有一个巨大的文件,我必须逐行解析。速度至关重要。

一行示例:

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 MB。

我只担心解析的执行时间。什么代码将在 Delphi (Pascal) 中产生绝对最快的执行速度?

【问题讨论】:

    标签: delphi parsing token pascal


    【解决方案1】:

    自己动手无疑是最快的方法。有关此主题的更多信息,您可以查看Synedit's source code,其中包含市场上任何语言的词法分析器(在项目上下文中称为荧光笔)。我建议您以其中一个词法分析器为基础,并根据自己的用途进行修改。

    【讨论】:

      【解决方案2】:

      我制作了一个基于状态引擎 (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 更清晰、更高效。
      【解决方案3】:

      我认为最大的瓶颈总是将文件放入内存。一旦你把它放在内存中(显然不是一次全部,但如果我是你,我会使用缓冲区),实际的解析应该是微不足道的。

      【讨论】:

      • 其实不是。一个 25 MB 文件的简单读取文件到缓冲区需要 0.04 秒,编码它需要 0.17 秒(将 ASCII 转换为 Unicode)。然后花了 4.5 秒来读取 2500 万个字符并解析出该行的各个部分。所以我需要解析器的速度。
      【解决方案4】:

      编写代码的最快方法可能是创建一个 TStringList 并将文本文件中的每一行分配给 CommaText 属性。默认情况下,空格是分隔符,因此每个标记将获得一个 StringList 项。

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

      不过,您可能会通过自己解析每一行来获得更好的性能。

      【讨论】:

      • 对不起。我并不是指“编写”代码的最快方法。我真的想要最快的代码。我现在正在编辑问题以使其显而易见。
      【解决方案5】:

      这是一个非常简单的词法分析器的蹩脚实现。这可能会给你一个想法。

      请注意此示例的限制 - 不涉及缓冲,没有 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 可以有效地确定令牌类型。
      【解决方案6】:

      这引出了另一个问题 - 有多大? 给我们一个线索,比如 # of lines 或 # 或 Mb (Gb)? 然后我们会知道它是否适合内存,是否需要基于磁盘等。

      首先我会使用我的 WordList(S: String; AList: TStringlist);

      然后您可以以 Alist[n] 的身份访问每个令牌... 或对它们进行排序或其他。

      【讨论】:

      • 没有。它很容易融入记忆。说 200 MB。假设它已经在 StringList 中。我将编辑问题并添加澄清这一点。
      【解决方案7】:
      • 使用 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;
      

      【讨论】:

        【解决方案8】:

        一旦解析,速度将始终与您正在执行的操作相关。到目前为止,词法解析器是从文本流转换为标记的最快方法,无论大小如何。类单元中的 TParser 是一个很好的起点。

        我需要编写解析器已经有一段时间了,但另一种更过时但尝试过且真正的方法是使用 LEX/YACC 构建语法,然后让它将语法转换为代码,您可以使用它来执行您的加工。 DYacc 是 Delphi 版本...不确定它是否仍然可以编译,但如果你想做老派的事情,值得一看。如果您能找到一份副本,这里的dragon book 会很有帮助。

        【讨论】:

          【解决方案9】:

          如果速度至关重要,那么自定义代码就是答案。查看将文件映射到内存的 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。我了解内存映射文件对于随机访问通常更快,但对于顺序访问则永远不会。此外,我仍然需要进行编码。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-04-12
          • 2016-10-27
          • 1970-01-01
          • 1970-01-01
          • 2010-11-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多