10

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

一行示例:

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) 中产生绝对最快的执行速度?

4

9 回答 9

34
  • 使用 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;
于 2008-11-13T20:36:23.213 回答
4

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

请注意此示例的限制 - 不涉及缓冲,没有 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.
于 2008-11-13T18:59:09.650 回答
3

我做了一个基于状态引擎(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;

它很简单,就像一个魅力。

于 2008-11-13T18:37:57.867 回答
2

如果速度至关重要,那么自定义代码就是答案。查看将文件映射到内存的 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;
于 2008-11-14T10:59:04.783 回答
1

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

于 2008-11-13T18:43:13.910 回答
1

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

在第一次通过时,我会使用我的 WordList(S: String; AList: TStringlist);

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

于 2008-11-13T19:45:14.580 回答
1

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

就我个人而言,我需要编写一个解析器已经有一段时间了,但另一种更过时但经过尝试和真正的方法是使用 LEX/YACC 来构建一个语法,然后让它将语法转换成你可以用来执行你的处理的代码。 DYacc是一个 Delphi 版本……不确定它是否仍然可以编译,但如果你想做老派的事情,值得一看。如果你能找到一本,这里的龙书会有很大帮助。

于 2008-11-13T21:12:11.380 回答
0

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

于 2008-11-13T18:36:55.640 回答
0

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

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

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

于 2008-11-13T18:56:09.017 回答