Delphi有快速的GetToken例程吗?

时间:2009-11-07 18:44:59

标签: delphi optimization parsing token aqtime

7 个答案:

答案 0 :(得分:12)

预计“Delim”会有什么不同。如果预期它是一个单个字符,那么你最好逐个字符地逐字逐句,最好通过PChar,并进行特定的测试。

如果它是一个长字符串,Boyer-Moore和类似搜索有跳过表的设置阶段,最好的方法是构建表一次,并为每个后续查找重用它们。这意味着你需要调用之间的状态,而这个函数最好作为一个对象的方法。

你可能会对this answer I gave to a question some time before, about the fastest way to parse a line in Delphi.感兴趣(但我看到你问的是问题!然而,在解决你的问题时,我会告诉我如何描述解析,使用PosEx就像你正在使用的那样,取决于Delim通常的样子。)


更新:好的,我花了大约40分钟来看这个。如果您知道分隔符将成为一个角色,那么第二个版本(即PChar扫描)几乎总是更好,但您必须将Delim作为角色传递。在撰写本文时,您将PLine^表达式(Char类型)转换为字符串,以便与Delim进行比较。那将是非常缓慢的;即使索引到字符串,Delim[1]也会有点慢。

但是,根据您的线条的大小以及要拉出多少分隔的部分,您可能最好采用可恢复的方法,而不是在标记化例程中跳过不需要的分隔部分。如果你通过连续增加索引调用GetTok,就像你目前在迷你基准测试中所做的那样,你最终会得到O(n * n)性能,其中n是分隔部分的数量。如果保存扫描状态并将其恢复到下一次迭代,或者将所有提取的项打包成数组,则可以将其转换为O(n)。

这是一个完成所有标记化的版本,并返回一个数组。它需要两次标记,以便知道制作数组有多大。另一方面,只有第二个标记化需要提取字符串:

// Do all tokenization up front.
function GetTok4(const Line: string; const Delim: Char): TArray<string>;
var
  cp, start: PChar;
  count: Integer;
begin
  // Count sections
  count := 1;
  cp := PChar(Line);
  start := cp;
  while True do
  begin
    if cp^ <> #0 then
    begin
      if cp^ <> Delim then
        Inc(cp)
      else
      begin
        Inc(cp);
        Inc(count);
      end;
    end
    else
    begin
      Inc(count);
      Break;
    end;
  end;

  SetLength(Result, count);
  cp := start;
  count := 0;

  while True do
  begin
    if cp^ <> #0 then
    begin
      if cp^ <> Delim then
        Inc(cp)
      else
      begin
        SetString(Result[count], start, cp - start);
        Inc(cp);
        Inc(count);
      end;
    end
    else
    begin
      SetString(Result[count], start, cp - start);
      Break;
    end;
  end;
end;

这是可恢复的方法。但是,当前位置和分隔符的加载和存储确实有成本:

type
  TTokenizer = record
  private
    FSource: string;
    FCurrPos: PChar;
    FDelim: Char;
  public
    procedure Reset(const ASource: string; ADelim: Char); inline;
    function GetToken(out AResult: string): Boolean; inline;
  end;

procedure TTokenizer.Reset(const ASource: string; ADelim: Char);
begin
  FSource := ASource; // keep reference alive
  FCurrPos := PChar(FSource);
  FDelim := ADelim;
end;

function TTokenizer.GetToken(out AResult: string): Boolean;
var
  cp, start: PChar;
  delim: Char;
begin
  // copy members to locals for better optimization
  cp := FCurrPos;
  delim := FDelim;

  if cp^ = #0 then
  begin
    AResult := '';
    Exit(False);
  end;

  start := cp;
  while (cp^ <> #0) and (cp^ <> Delim) do
    Inc(cp);

  SetString(AResult, start, cp - start);
  if cp^ = Delim then
    Inc(cp);
  FCurrPos := cp;
  Result := True;
end;

Here's the full program I used for benchmarking.

结果如下:

*** count=3, Length(src)=200
GetTok1: 595 ms
GetTok2: 547 ms
GetTok3: 2366 ms
GetTok4: 407 ms
GetTokBK: 226 ms
*** count=6, Length(src)=350
GetTok1: 1587 ms
GetTok2: 1502 ms
GetTok3: 6890 ms
GetTok4: 679 ms
GetTokBK: 334 ms
*** count=9, Length(src)=500
GetTok1: 3055 ms
GetTok2: 2912 ms
GetTok3: 13766 ms
GetTok4: 947 ms
GetTokBK: 446 ms
*** count=12, Length(src)=650
GetTok1: 4997 ms
GetTok2: 4803 ms
GetTok3: 23021 ms
GetTok4: 1213 ms
GetTokBK: 543 ms
*** count=15, Length(src)=800
GetTok1: 7417 ms
GetTok2: 7173 ms
GetTok3: 34644 ms
GetTok4: 1480 ms
GetTokBK: 653 ms

根据数据的特征,分隔符是否可能是字符,以及如何使用它,不同的方法可能会更快。

(我在之前的程序中犯了一个错误,我没有为每种常规测量相同的操作。我更新了pastebin链接和基准测试结果。)

答案 1 :(得分:11)

您的新功能(具有PChar的功能)应将“Delim”声明为 Char 而不是 String 。在您当前的实现中,编译器必须将PLine ^ char转换为字符串,以将其与“Delim”进行比较。这种情况发生在一个紧密的循环中,导致了巨大的性能损失。

function GetTok(const Line: string; const Delim: Char{<<==}; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
{ LK Nov 7, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; http://stackoverflow.com/questions/1694001/is-there-a-fast-gettoken-routine-for-delphi }
var
 I: integer;
 PLine, PStart: PChar;
begin
  PLine := PChar(Line);
  PStart := PLine;
  inc(PLine);
  for I := 1 to TokenNum do begin
    while (PLine^ <> #0) and (PLine^ <> Delim) do
      inc(PLine);
    if I = TokenNum then begin
      SetString(Result, PStart, PLine - PStart);
      break;
    end;
    if PLine^ = #0 then begin
      Result := '';
      break;
    end;
    inc(PLine);
    PStart := PLine;
  end;
end; { GetTok }

答案 2 :(得分:9)

Delphi编译为非常高效的代码;根据我的经验,在汇编程序中做得更好是非常困难的。

我认为你应该在字符串的开头指出一个PChar(它们仍然存在,不是吗?我在Delphi 4.0之间分道扬and)并在计算“|”s时递增它,直到你找到其中n-1。我怀疑这会比反复拨打PosEx更快。

记下该位置,然后再将指针递增一点,直到你碰到下一个管道。拉出你的子串。完成。

我只是猜测,但如果这个问题接近最快,我就不会感到惊讶。

编辑:这就是我的想法。唉,这段代码是未经编译和未经测试的,但它应该证明我的意思。

特别是,Delim被视为一个单一的字符,我相信如果能够满足要求,那么它将成为一个与众不同的世界,并且PLine中的角色只会被测试一次。最后,没有与TokenNum的比较;我认为将计数器减少到0来计算分隔符会更快。

function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
var 
  Del: Char;
  PLine, PStart: PChar;
  Nth, I, P0, P9: Integer;
begin
  Del := Delim[1];
  Nth := TokenNum + 1;
  P0 := 1;
  P9 := Line.length + 1;
  PLine := PChar(line);
  for I := 1 to P9 do begin
    if PLine^ = Del then begin
      if Nth = 0 then begin
        P9 := I;
        break;
      end;
      Dec(Nth);
      if Nth = 0 then P0 := I + 1
    end;
    Inc(PLine);
  end;
  if (Nth <= 1) or (TokenNum = 1) then
    Result := Copy(Line, P0, P9 - P0);
  else
    Result := '' 
end;

答案 3 :(得分:2)

使用汇编程序将是一种微优化。通过优化算法可以获得更大的收益。不做工作每次都以最快的方式进行工作。

一个例子是,如果你的程序中有多个地方需要同一行的几个令牌。另一个返回一个令牌数组然后可以索引的过程应该比多次调用你的函数更快,特别是如果你让程序没有返回所有的令牌,只有你需要的那么多。

但总的来说,我同意Carl的答案(+1),使用PChar进行扫描可能会比您当前的代码更快。

答案 4 :(得分:1)

这是我在个人图书馆中使用了很长一段时间的功能,我广泛使用。我相信这是它的最新版本。我过去有多个版本因各种原因而进行了优化。这个尝试考虑引用字符串,但如果删除该代码,它会使函数稍微快一点。

我实际上有许多其他例程,CountSections和ParseSectionPOS就是几个例子。

不幸的是,这个例程仅基于ansi / pchar。虽然我认为将它移到unicode并不困难。也许我已经做到了......我必须检查一下。

注意:此例程基于ParseNum索引为1。

function ParseSection(ParseLine: string; ParseNum: Integer; ParseSep: Char; QuotedStrChar:char = #0) : string;
var
   wStart, wEnd : integer;
   wIndex : integer;
   wLen : integer;
   wQuotedString : boolean;
begin
   result := '';
   wQuotedString := false;
   if not (ParseLine = '') then
   begin
      wIndex := 1;
      wStart := 1;
      wEnd := 1;
      wLen := Length(ParseLine);
      while wEnd <= wLen do
      begin
         if (QuotedStrChar <> #0) and (ParseLine[wEnd] = QuotedStrChar) then
            wQuotedString := not wQuotedString;

         if not wQuotedString and (ParseLine[wEnd] = ParseSep) then
         begin
            if wIndex=ParseNum then
               break
            else
            begin
               inc(wIndex);
               wStart := wEnd+1;
            end;
         end;
         inc(wEnd);
      end;

      result := copy(ParseLine, wStart, wEnd-wStart);
      if (length(result) > 0) and (QuotedStrChar <> #0) and (result[1] = QuotedStrChar) then
         result := AnsiDequotedStr(result, QuotedStrChar);
   end;
end; { ParseSection }

答案 5 :(得分:1)

在您的代码中,我认为这是唯一可以优化的行:

Result := copy(Line, P+1, MaxInt)

如果你计算那里的新长度,它可能会快一点,但不是你要找的10%。

您的令牌化算法似乎很好。 为了优化它,我将使用生产数据的代表性子集通过分析器(如来自AutomatedQA的AQTime)运行它。这将指向你最薄弱的地方。

唯一接近的RTL函数是Classes单元中的这个函数:

procedure TStrings.SetDelimitedText(const Value: string);

它标记,但同时使用 QuoteChar 分隔符,但您只使用分隔符。

它使用系统单元中的SetString函数,这是一种基于PChar / PAnsiChar / PUnicodeChar和长度设置字符串内容的快速方法。

这也可能会让你有所改善;另一方面,复制也非常快。

答案 6 :(得分:1)

我不是总是责怪算法的人,但是如果我看一下第一个来源, 问题是对于字符串N,你也会再次为字符串1..n-1做POS / posexes。

这意味着对于N个项目,你总和(n,n-1,n-2 ... 1)POSes(= + / - 0.5 * N ^ 2),而只需要N.

如果您只是缓存最后找到的结果的位置,例如在VAR参数传递的记录中,您可以获得很多收益。

键入
     TLastPosition =记录                        elementnr:整数; //最后一个令牌号                        elementpos:整数; //上次匹配的字符索引                        结束;

然后是

如果tokennum =(lastposition.elementnr + 1)则     开始       newpos:= posex(DELIM,线,lastposition.elementpos);     端;

不幸的是,我现在没有时间写出来,但我希望你明白这个想法