【发布时间】:2010-12-14 05:16:53
【问题描述】:
在我的程序中,我处理了数百万个具有特殊字符的字符串,例如“|”分隔每个字符串中的标记。我有一个函数可以返回第 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's: 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 类似的问题: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;
所以这将测试对 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)。
我还想在这里跟进其他 cmets 并进行测试。
关闭优化并打开字符串格式检查只会将时间从 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 社区。p>
【问题讨论】:
-
为什么要处理数百万个字符串?也许您的程序可以进行优化,使其不必这样做。
-
这个问题是我优化的尝试之一。当您有一个处理 300 MB 文件的程序时,无论如何它都必须做大量的工作、优化和技巧。但是,如果输入文件对我的程序来说那么大,如果不先处理它,我就无法将它变小。
-
感谢下午 1.01 的链接(一个有趣的讨论),但我相信如果偏离主题的内容以其他方式传输,例如,StackOverflow 社区将不胜感激。作为对我博客的评论。
-
您的实现仍然有一个非常错误的地方:检查 PChar 的字符串结尾是不必要的,因为您开始使用的字符串包含它的长度以非常快速的管理形式,因此您可以将其用作 for 的最终值。不过,这需要更多的思考。我会这样做并回复你。
-
我已将我的想法转化为代码。我很想知道它的表现如何!
标签: delphi optimization parsing token aqtime