Delphi中有一个快速的GetToken程序吗?

21

在我的程序中,我处理了数百万个具有特殊字符的字符串,例如 "|" 来分隔每个字符串中的标记。我有一个函数来返回第n个标记,这就是它:

function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
var
 I, P, P2: integer;
begin
  P2 := Pos(Delim, Line);
  if TokenNum = 1 then begin
    if P2 = 0 then
      Result := Line
    else
      Result := copy(Line, 1, P2-1);
  end
  else begin
    P := 0; { To prevent warnings }
    for I := 2 to TokenNum do begin
      P := P2;
      if P = 0 then break;
      P2 := PosEx(Delim, Line, P+1);
    end;
    if P = 0 then
      Result := ''
    else if P2 = 0 then
      Result := copy(Line, P+1, MaxInt)
    else
      Result := copy(Line, P+1, P2-P-1);
  end;
end; { GetTok }

我开发了这个函数,当时我使用的是Delphi 4。它调用了非常高效的PosEx例程,它最初是由Fastcode开发的,现在包含在Delphi的StrUtils库中。

我最近升级到了Delphi 2009,并且我的字符串都是Unicode。这个GetTok函数仍然可以工作,并且仍然很好地工作。

我已经浏览了Delphi 2009中的新库,其中有许多新的函数和增强功能。

但我在任何新的Delphi库中都没有看到我需要的GetToken函数,在各种fastcode项目中也找不到任何东西,除了Zarko Gajic的:Delphi Split / Tokenizer Functions,但它没有我已经拥有的那么优化。

即使改进10%,在我的程序中也会明显。我知道另一种方法是使用StringLists并始终保持标记分开,但这在内存方面有很大的开销,并且我不确定如果我做了所有的转换工作,它是否会更快。

哎呀。所以在这篇漫长的文章之后,我的问题真的是:

您是否知道任何非常快速实现GetToken例程的方法?汇编优化版本将是理想的吗?

如果没有,您能否看到上面的代码中有任何优化可能会改善情况?


后续:Barry Kelly提到了我一年前关于优化文件中行的解析的问题。那时我甚至没想到我的GetTok例程,因为它并未用于读取或解析。直到我看到了我的GetTok例程的开销,才引发了这个问题。在Carl Smotricz和Barry的回答之前,我从未想过将两者联系起来。这太显而易见了,但我没有意识到。感谢指出这一点。

是的,我的Delim是一个单字符,所以显然我可以做出一些重大的优化。我在GetTok例程中使用Pos和PosEx(上面),这使我对使用字符搜索忽视了一些更快的方法,例如使用以下代码片段:

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

我将逐一查看每个人的答案,尝试不同的建议并进行比较。然后我会发布结果。


困惑:好吧,现在我真的很困惑。

我采用了卡尔和巴里的建议选择使用PChars,这是我的实现:

function GetTok(const Line: string; const Delim: string; 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; https://dev59.com/s-o6XIcBkEYKwwoYTzEw }
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 }

在实际情况下,我认为这已经是最好的了。

因此,我将两个例程都用于任务,并使用AQTime查看发生了什么。我的运行包括1,108,514次对GetTok的调用。

AQTime计时原始例程为0.40秒。一百万次Pos的调用花费了0.10秒。五十万个TokenNum = 1的副本花费了0.10秒。六十万个PosEx调用只花费了0.03秒。

然后,我使用AQTime为相同的运行和完全相同的调用计时了我的新例程。AQTime报告说,我的新“快速”例程需要3.65秒,即比原来慢9倍。根据AQTime的说法,罪魁祸首是第一个循环:

     while (PLine^ <> #0) and (PLine^ <> Delim) do
       inc(PLine);

这个 while 循环被执行了 1800 万次,耗时 2.66 秒。而 inc 行则被执行了 1600 万次,用时为 0.47 秒。

我原以为自己知道这里发生了什么。去年我在一个问题中使用 AQTime 遇到了类似的问题:为什么 CharInSet 比 Case 语句快?

再次感谢 Barry Kelly 提供了线索。基本上,像 AQTime 这样的插装分析器并不一定适合进行微优化。它会给每一行代码都增加开销,这可能会淹没结果,正如这些数字所显示的那样。在我的新“优化代码”中执行的 3400 万行代码压倒了原始代码的数百万行,Pos 和 PosEx 程序显然几乎没有任何开销。

Barry 给了我一个使用 QueryPerformanceCounter 的代码示例来证明他是正确的,在那个例子中他是对的。

好的,现在让我们使用 QueryPerformanceCounter 来证明我的新程序更快,而不是 AQTime 所说的慢了 9 倍。所以这就是我要做的:

function TimeIt(const Title: string): double;
var  i: Integer;
  start, finish, freq: Int64;
  Seconds: double;
begin
  QueryPerformanceCounter(start);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 1);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 2);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 3);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 4);
  QueryPerformanceCounter(finish);
  QueryPerformanceFrequency(freq);
  Seconds := (finish - start) / freq;
  Result := Seconds;
end;

这将测试1,000,000次对GetTok的调用。

我的旧过程使用Pos和PosEx调用,耗时0.29秒。 使用PChars的新过程需要2.07秒。

现在我完全困惑了!有人能告诉我为什么PChar过程不仅更慢,而且慢8至9倍吗?


谜底揭开了!Andreas在他的回答中说将Delim参数从字符串更改为Char。我将始终只使用Char,因此至少对于我的实现来说,这是非常可能的。我对发生的事情感到惊讶。

100万次调用的时间从1.88秒降至0.22秒。

令人惊讶的是,当我将原始的Pos / PosEx例程的Delim参数更改为Char时,其时间从0.29秒上升到0.44秒。

坦白地说,我对Delphi的优化器感到失望。Delim是一个常量参数。优化器应该注意到在循环中发生了相同的转换,并应该将其移出,以便只进行一次。

双重检查我的代码生成参数,是的,我有优化开启,字符串格式检查关闭。

底线是,经过Andrea的修复,新的PChar例程大约比我的原始例程快25%(.22与.29)。

我仍然想跟进这里的其他评论并测试它们。


关闭优化并打开字符串格式检查仅将时间从.22增加到.30。它对原始内容增加的约相同。

使用汇编代码或调用用汇编语言编写的例程(如Pos或PosEx)的优点在于它们不受您设置的代码生成选项的影响。它们始终以预优化和非膨胀的方式运行。

在过去的几天中,我已经确认了微优化代码的最佳方法是查看和比较CPU窗口中的汇编代码。如果Embarcadero能够使该窗口更加方便,并允许我们将部分复制到剪贴板或打印部分,则会很好。

此外,在此帖子中,我不公平地抨击了AQTime,认为它添加的额外时间仅是因为它添加的仪器。现在我回头检查Char参数而不是String时,while循环的时间从2.66降至.30秒,而inc行的时间从.47降至.14秒。奇怪的是,inc行的时间也会下降。但我已经筋疲力尽地进行了所有这些测试

function GetTok(const Line: string; const Delim: Char; const TokenNum: Byte): string;
{ LK Nov 8, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; https://dev59.com/s-o6XIcBkEYKwwoYTzEw }
var
  I, CurToken: Integer;
  PLine, PStart: PChar;
begin
  CurToken := 1;
  PLine := PChar(Line);
  PStart := PLine;
  for I := 1 to length(Line) do begin
    if PLine^ = Delim then begin
      if CurToken = TokenNum then
        break
      else begin
        CurToken := CurToken + 1;
        inc(PLine);
        PStart := PLine;
      end;
    end
    else
      inc(PLine);
  end;
  if CurToken = TokenNum then
    SetString(Result, PStart, PLine - PStart)
  else
    Result := '';
end;

可能还有一些小的优化可以进行,比如CurToken = Tokennum这个比较,应该使用相同类型的Integer或Byte,以速度更快的那个为准。

但是现在我很满意了。

再次感谢StackOverflow Delphi社区。


这个问题是我优化的尝试之一。当你有一个处理300MB文件的程序时,无论如何都需要进行大量的工作、优化和技巧。但如果输入文件这么大,没有办法在未经处理之前就将其缩小。 - lkessler
谢谢你在下午1:01提供的链接(一个有趣的讨论),但我相信StackOverflow社区会更欣赏如果离题的内容通过其他方式传递,例如作为我的博客评论。 - lkessler
我已经将我的想法转化为代码。我很有兴趣听听它的表现如何! - Carl Smotricz
每次都将PChar指针转换为字符串。要么与Delim [1]进行比较,要么将Delim参数更改为Char。 - Barry Kelly
我更新了我的回答。最终结果:这取决于情况。如果你的分隔符可能超过一个字符,那么你原来的例程还不错,尽管使用Boyer-Moore可能会更快。如果它是单个字符,则PChar扫描将胜过它。顺便说一句,如果速度非常重要且数据源不是UTF-16,则使用AnsiString和PAnsiChar会更快。 - Barry Kelly
显示剩余3条评论
7个回答

12

如果"Delim"预期是一个单一的字符,那么它的期望值非常重要。最好通过PChar逐个字符地遍历字符串并进行特定测试。

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

您可能会对我以前回答的有关在Delphi中解析行的最快方法的问题感兴趣。(但我看到这是您提出的问题!尽管如此,在解决您的问题时,我会坚持我所描述的解析方式,像您正在使用的PosEx,具体取决于Delim通常的外观。)


更新: 好的,我花了大约40分钟查看了这个。如果您知道定界符将是一个字符,那么第二个版本(即PChar扫描)通常更好,但您必须将Delim作为一个字符传递。在编写时,您正在将类型为Char的PLine^表达式转换为字符串以与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;

这是我用于基准测试的完整程序。

以下是结果:

*** 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链接和基准测试结果。)


Barry:感谢您的回复。请查看我在问题中的“跟进”。 - lkessler
不错... +1!GetTok3为什么这么慢的原因实际上可以在编译器选项中启用了“字符串格式检查”的开关中找到。关闭此开关并重复测量! - GJ.
Barry:我的最终“最佳”代码是通过PChar循环而不是通过Token循环,这与您的前置标记化非常相似。这可能是这种类型问题的最佳选择,并表明了一种漂亮的通用方法,即通过字符串进行顺序处理以实现快速执行。 - lkessler

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; https://dev59.com/s-o6XIcBkEYKwwoYTzEw }
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 }

你做到了,Andreas!拼图已经解决了,并警告那些在传递字符串时可以使用字符的人们。 - lkessler

9
Delphi编译出的代码非常高效。以我的经验来看,在汇编语言中很难做得更好。 我觉得你应该只需将PChar指向字符串的开头,然后在计算"|"数时递增它,直到找到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;

1
我大约有80%的把握。毕竟,PChar仍然被用作字符指针。我猜想Inc运算符会将其移动一定Unicode字符的宽度。 - Carl Smotricz
是的,它可以很好地处理Unicode,正如我上面的实现清楚地展示的那样。但在给出一些解释之前,它看起来要慢得多。 - lkessler
我更新了我的回答。最终结果:这取决于情况。如果你的分隔符可能超过一个字符,那么你原来的例程还不错,尽管使用Boyer-Moore算法可能会更快。如果它是单个字符,则PChar扫描将胜过它。顺便说一句,如果速度非常重要且数据源不是UTF-16,则使用AnsiString和PAnsiChar会更快。 - Barry Kelly
谢谢你的帮助,Carl。我有一个问题的更新,我已经添加了你的代码实现,它比之前快了约15%。 - lkessler
0.19秒...太棒了!我很高兴听到这个消息,也非常感谢您的反馈。是的,我认为TokenNum应该是一个整数;字节可以节省空间,但对于单个字节,CPU需要不必要地操作和掩码才能处理它们。如果可能的话,你应该递减它,这样你就可以与0进行比较。 - Carl Smotricz
显示剩余2条评论

2
使用汇编语言只是微小的优化。通过优化算法可以获得更大的收益。每次都在最快的方式下执行工作,不如不去执行。
例如,如果您的程序中需要同一行的多个标记,则返回一个标记数组的另一个过程应该比调用函数更快,特别是如果让该过程不返回所有标记,而仅返回您需要的标记数量。
但总的来说,我同意卡尔的答案(+1),使用PChar进行扫描可能比您当前的代码更快。

绝对的,首先我会优化算法。我希望在过去的十年中已经完成了大部分工作。现在是时候再从中挤出一点更多的血液了。但有趣的是,从微观层面来看确实能够给你宏观层面的见解。我现在正在进行各种改进,只是因为我再次思考这些问题。 - lkessler

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 }

感谢提供代码。您会很高兴知道,它在Delphi 2009中使用Unicode字符串正常工作。使用上面描述的QueryPerformanceCounter进行计时(使用1,000,000次调用),QuotedStrChar代码保留的时间为0.74秒。我将该代码删除并再次尝试,这将其缩短至0.56秒。这仍然比我的原始Pos / PosEX代码慢,后者只需0.29秒。 - lkessler

1
在你的代码中,我认为这是唯一可以优化的行:
Result := copy(Line, P+1, MaxInt)

如果你在那里计算新的长度,速度可能会快一些,但不会达到你寻找的10%。

你的分词算法看起来还不错。 为了优化它,我建议你使用一个分析器(比如AutomatedQA的AQTime)来对生产数据的代表子集进行分析。这将指出最薄弱的环节。

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

procedure TStrings.SetDelimitedText(const Value: string);

它进行了标记化,但同时使用了QuoteCharDelimiter,而你只使用了一个Delimiter。

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

这也许会给你带来一些改进;另一方面,Copy也非常快速。


看了你的第一点,我认为你对MaxInt是错误的。要计算长度,应该是:length(Line) - P,而这个减法比使用常量MaxInt更昂贵。Delphi不在乎复制的长度是否超过字符串的末尾。它知道在字符串完成时停止。我已经使用“MaxInt”技巧很长时间了,在某个地方推荐后 - 我不记得了。每次编码时,它可以节省我5秒钟。 :-) - lkessler
TStrings.SetDelimitedText函数旨在将字符串添加到字符串列表中,而不是挑选出一个特定的标记。但它使用了与我上面描述的被认为是最佳的PChar方法类似的技术。我也使用了SetString,它非常快速。AQTime报告说,对SetString的1.7百万次调用只需要0.05秒。 - lkessler
@lkessler:实际上,SetDelimitedText是用于替换字符串列表的内容。但你理解了我的意思:它使用非常相似的技术,但基于PChar(正如Carl和Bary建议的那样),所以值得一看。很好,你验证了MaxInt的问题:我指出它可能会改进,但你测量后发现MaxInt是最佳选择。我浏览了所有新的评论和你的问题编辑,看起来你已经解决了。太棒了!我非常喜欢这种stackoverflow社区的工作方式。 - Jeroen Wiert Pluimers

1

我并不总是责怪算法的人,但如果我看一下第一段源代码,问题在于对于字符串N,您也会对字符串1..n-1进行POS/posexes。

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

如果您简单地缓存上次找到结果的位置,例如在通过VAR参数传递的记录中,您可以获得很多好处。

类型
TLastPosition = 记录 elementnr : 整数; // 上一个标记号 elementpos: 整数; // 上一个匹配的字符索引 end;

然后是一些内容

如果tokennum =(lastposition.elementnr + 1)那么 begin newpos:= posex(delim,line,lastposition.elementpos); end;

不幸的是,我现在没有时间写出来,但我希望您能理解我的意思。


好的,重写的算法完全摆脱了Pos和PosEXs。但是你的想法在优化原始算法方面很不错。 - lkessler
1
@lkessler:这个观点同样适用于重写的算法,这就是我在我的答案中所说的。如果您连续从同一字符串中获取前5个标记,则第一个标记将扫描5次,第二个标记将扫描4次,...返回所有5个标记的不同过程应该更快,如果您注意如何返回结果(不重新分配数组)。这与是否使用PosEx()无关。对于重写的算法,您可以返回令牌地址并将其用作下一个函数调用的搜索起点。 - mghie
mghie:是的。说得好。最好的方法可能是实现GetFirstTok和GetNextTok,以便在需要按顺序获取它们的情况下使用。 - lkessler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接