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,P2-P-1); end; end; { GetTok }
当我使用Delphi 4时,我开发了这个功能。它调用了最初由Fastcode开发的PosEx例程,现在包含在Delphi的StrUtils库中。
我最近升级到Delphi 2009,我的字符串都是Unicode。这个GetTok功能仍然可以正常工作。
我在德尔福2009年经历了新的图书馆,并且有很多新的功能和补充。
但是我没有看到一个GetToken函数,就像我需要的任何新的Delphi库,在各种fastcode项目中,除了Zarko Gajic’s: Delphi Split / Tokenizer Functions之外,我没有找到与Google搜索相关的任何东西,这并不像我已经有的那样优化。
任何改进,甚至10%将在我的计划中显而易见。我知道一个替代方法是StringLists,并始终保持令牌分开,但这有一个大开销的内存明智,我不知道我是否做了所有的工作来转换是否会更快。
呼。所以经过这么长时间的谈话,我的问题是:
你知道GetToken例程的任何非常快速的实现?汇编器优化版本会是理想的吗?
如果没有,是否有任何优化,您可以看到我的代码上面可能会有所改进?
跟随:Barry Kelly提到了一年前我问的关于优化文件中行的解析的问题。那时候我甚至没想到我的GetTok例程,没有用于读取或解析。现在只有我看到我的GetTok例程的开销导致我问这个问题。直到卡尔·斯科特里奇和巴里的答案,我从来没有想过连接这两个。很明显,但它没有注册。感谢您指出了这一点。
是的,我的Delim是一个单一的角色,显然我有一些主要的优化我可以做。我在GetTok例程(上图)中使用了Pos和PosEx,这使我觉得我可以用字符搜索来更快地进行更改,代码如下:
while (cp^ > #0) and (cp^ <= Delim) do Inc(cp);
我将要经过大家的答案,并尝试各种建议并进行比较。然后我会发布结果。
混乱:好的,现在我真的很困惑
我把Carl和Barry的建议和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; 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 }
在纸上,我不认为你可以做得比这更好。
所以我把这两个例程都放在这个任务中,并用AQTime来看看发生了什么。运行我已经包括1,108,514个调用GetTok。
AQTime将原始程序定时在0.40秒。对Pos的百万电话花费了0.10秒。令牌半数= 1份,共有五十万次,花费了0.10秒。 60万PosEx电话只需要0.03秒。
然后我用同样的运行和完全相同的电话为AQTime定时了我的新例程。 AQTime报告说,我的新“快速”例程花费了3.65秒,这是9倍。根据AQTime的罪魁祸首是第一个循环:
while (PLine^ <> #0) and (PLine^ <> Delim) do inc(PLine);
被执行了1800万次的“同行”报道为2.66秒。执行了1600万次的这个线,据说要花费0.47秒。
现在我以为我知道这里发生了什么。我在去年提出的一个问题上与AQTime有类似的问题:Why is CharInSet faster than Case statement?
再次是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秒下降到了.22秒。
令人惊讶的是,我的原始Pos / PosEx例程的时间从.29到.44秒,当我将Delim参数改为Char时。
坦白说,我对Delphi的优化器感到失望。那个Delim是一个常数参数。优化器应该注意到循环中发生相同的转换,并且应该将其移出,以使它只能执行一次。
双重检查我的代码生成参数,是的,我确实有优化True和String格式检查关闭。
底线是安德烈修复的新PChar例程比我原来的(.22对.29)快25%。
我仍然想跟进这里的其他评论,并测试出来。
关闭优化和打开String格式检查只会将时间从.22增加到.30。它增加了与原来相同的。
使用汇编代码或者调用像Pos或PosEx这样的汇编器编写的例程的优点是它们不受您设置的代码生成选项的约束。他们将永远运行相同的方式,预先优化和不blo肿的方式。
我在过去几天重申,比较微型优化代码的最佳方式是查看并比较cpu窗口中的汇编代码。如果Embarcadero可以使该窗口更方便,那么这将是很好的,并允许我们将部分复制到剪贴板或打印部分。
此外,我在这篇文章中早些时候不公平地抨击了AQTime,认为为我的新例程添加的额外时间仅仅是因为它添加的工具。现在我回去查看Char参数而不是String,while循环下降到.30秒(从2.66开始),而inc行下降到.14秒(从.47)。奇怪的是,线路也会下降。但我已经从所有这些测试中磨损了。
我把卡尔的想法用字符循环,并用这个想法重写了这个代码。它进一步改善,从.22秒下降到19秒。所以现在到现在为止最好:
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; http://stackoverflow.com/questions/1694001/is-there-a-fast-gettoken-routine-for-delphi } 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,PLine - PStart) else Result := ''; end;
仍然可能会有一些小的优化,例如CurToken = Tokensum比较,它应该是相同的类型,整数或字节,取其中较快者。
但是让我们说,我现在很开心。
再次感谢StackOverflow Delphi社区。
解决方法
function GetTok(const Line: string; const Delim: Char{<<==}; const TokenNum: Byte): string; { LK Feb 12,PLine - PStart); break; end; if PLine^ = #0 then begin Result := ''; break; end; inc(PLine); PStart := PLine; end; end; { GetTok }