在我的程序中,我处理了数百万个具有特殊字符的字符串,例如“|” 分隔每个字符串中的标记。我有一个返回第 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 库和各种 fastcode 项目中看到像我需要的 GetToken 函数,而且除了Zarko Gajic 的:Delphi Split / Tokenizer Functions之外,我无法通过 Google 搜索找到任何东西,这不是和我已经拥有的一样优化。
在我的程序中,任何改进,即使是 10% 也会很明显。我知道另一种选择是 StringLists 并始终将标记分开,但这在内存方面有很大的开销,我不确定我是否做了所有的工作来转换它是否会更快。
唷。所以在所有这些冗长的谈话之后,我的问题真的是:
您知道 GetToken 例程的任何非常快速的实现吗?汇编程序优化版本是理想的吗?
如果没有,您是否可以看到我上面的代码的任何优化可能会有所改进?
跟进:Barry Kelly 提到了我一年前提出的一个关于优化文件中行的解析的问题。那时我什至没有想到我的 GetTok 例程没有用于读取或解析。直到现在我才看到我的 GetTok 例程的开销导致我提出这个问题。在 Carl Smotricz 和 Barry 给出答案之前,我从未想过将两者联系起来。如此明显,但它只是没有注册。感谢您指出了这一点。
是的,我的 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; https://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 来查看发生了什么。我的运行包括对 GetTok 的 1,108,514 次调用。
AQTime 将原始程序计时为 0.40 秒。对 Pos 的百万次调用只用了 0.10 秒。半百万的 TokenNum = 1 个副本需要 0.10 秒。600,000 个 PosEx 调用只用了 0.03 秒。
然后我用 AQTime 为我的新例程计时,以进行相同的运行和完全相同的调用。AQTime 报告说我的新“快速”程序耗时 3.65 秒,是 9 倍。根据 AQTime 的罪魁祸首是第一个循环:
while (PLine^ <> #0) and (PLine^ <> Delim) do
inc(PLine);
执行了 1800 万次的 while 行被报告为 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;
所以这将测试对 GetTok 的 1,000,000 次调用。
我使用 Pos 和 PosEx 调用的旧程序耗时 0.29 秒。带有 PChars 的新版本耗时 2.07 秒。
现在我完全糊涂了!谁能告诉我为什么 PChar 程序不仅慢,而且慢 8 到 9 倍!?
谜团已揭开!Andreas 在他的回答中说将 Delim 参数从字符串更改为 Char。我将永远只使用一个字符,所以至少对于我的实现来说这是很有可能的。我对发生的事情感到惊讶。
100 万次调用的时间从 1.88 秒下降到 0.22 秒。
令人惊讶的是,当我将它的 Delim 参数更改为 Char 时,我原来的 Pos/PosEx 例程的时间从 0.29 秒增加到 0.44 秒。
坦率地说,我对 Delphi 的优化器很失望。Delim 是一个常数参数。优化器应该已经注意到循环中发生了相同的转换,并且应该将其移出以便只执行一次。
仔细检查我的代码生成参数,是的,我确实有优化 True 和字符串格式检查关闭。
底线是 Andrea 修复的新 PChar 例程比我原来的快 25%(0.22 对 0.29)。
我仍然想在这里跟进其他评论并对其进行测试。
关闭优化并打开字符串格式检查只会将时间从 0.22 增加到 0.30。它在原版的基础上增加了大约相同的内容。
使用汇编程序代码或调用用 Pos 或 PosEx 等汇编程序编写的例程的优点是它们不受您设置的代码生成选项的影响。它们将始终以相同的方式运行,一种预先优化且不臃肿的方式。
我在过去几天重申,比较微优化代码的最佳方法是查看和比较 CPU 窗口中的汇编程序代码。如果 Embarcadero 可以使该窗口更方便一点,并允许我们将部分复制到剪贴板或打印它的部分,那就太好了。
此外,我在这篇文章的前面不公平地抨击了 AQTime,认为为我的新例程增加的额外时间仅仅是因为它添加了仪器。现在我返回并检查 Char 参数而不是 String,while 循环下降到 0.30 秒(从 2.66 开始),而 inc 行下降到 0.14 秒(从 0.47 开始)。奇怪的是,inc 线也会下降。但是我已经厌倦了所有这些测试。
我采用了 Carl 的按字符循环的想法,并用这个想法重写了该代码。它做出了另一项改进,从 0.22 秒降至 0.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; https://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, PStart, PLine - PStart)
else
Result := '';
end;
对此可能还有一些小的优化,比如 CurToken = Tokennum 比较,应该是相同的类型,Integer 或 Byte,以较快者为准。
但是,让我们说,我现在很高兴。
再次感谢 StackOverflow Delphi 社区。