18

我很困惑。今天在 CodeRage,Marco Cantu 说 CharInSet 很慢,我应该尝试使用 Case 语句。我在我的解析器中这样做了,然后用 AQTime 检查了加速是什么。我发现 Case 语句要慢得多。

4,894,539 次处决:

而不是 CharInSet (P^, [' ', #10,#13, #0]) 做 inc(P);

时间为 0.25 秒。

但相同数量的执行:

而 True do
  case P^ of
    ' ', #10, #13, #0: break;
    其他公司(P);
  结尾;

“while True”需要 0.16 秒,第一种情况需要 0.80 秒,else 情况需要 0.13 秒,总共需要 1.09 秒,或超过 4 倍。

CharInSet 语句的汇编代码是:

add edi,$02
mov edx,$0064b290
movzx eax,[edi]
call CharInSet
test a1,a1
jz $00649f18(回到add语句)

而案例逻辑就是这样:

movzx eax,[edi]
sub ax,$01
jb $00649ef0
sub ax,$09
jz $00649ef0
sub ax,$03
jz $00649ef0
add edi,$02
jmp $00649ed6(回到 movzx 语句)

在我看来,案例逻辑使用了非常高效的汇编程序,而 CharInSet 语句实际上必须调用 CharInSet 函数,该函数位于 SysUtils 中,也很简单,即:

函数 CharInSet(C: AnsiChar; const CharSet: TSysCharSet): 布尔值;
开始
Result := C in CharSet;
结尾;

我认为这样做的唯一原因是因为 [' ', #10, #13, #0] 中的 P^ 在 Delphi 2009 中不再允许,因此调用会转换类型以允许它。

尽管如此,我对此感到非常惊讶,但仍然不相信我的结果。

AQTime 测量是否有问题,我是否在比较中遗漏了什么,或者 CharInSet 真的是一个值得使用的高效函数?


结论:

我想你明白了,巴里。感谢您抽出宝贵时间做详细的示例。我在我的机器上测试了你的代码,得到了 0.171、0.066 和 0.052 秒(我猜我的台式机比你的笔记本电脑快一点)。

在 AQTime 中测试该代码,它给出了:三个测试的 0.79、1.57 和 1.46 秒。在那里,您可以看到仪器的大量开销。但真正让我吃惊的是,这种开销将明显的“最佳”结果更改为实际上是最差的 CharInSet 函数。

所以 Marcu 是正确的,而 CharInSet 更慢。但是您无意中(或者可能是故意)通过提取 CharInSet 对 Set 方法中的 AnsiChar(P^) 所做的事情,给了我一个更好的方法。除了与 case 方法相比速度稍有优势之外,它还比使用 case 方法更少的代码和更易于理解。

您还让我意识到使用 AQTime(和其他检测分析器)进行错误优化的可能性。知道这一点将有助于我对 Delphi 的 Profiler 和内存分析工具做出决定,这也是对我的问题AQTime 如何做到这一点的另一个答案?. 当然,AQTime 在检测时不会更改代码,因此它必须使用其他魔法来完成。

所以答案是 AQTime 显示的结果会导致错误的结论。


跟进:我留下了这个问题,“指责”AQTime 结果可能具有误导性。但公平地说,我应该指导您阅读这个问题:Delphi 是否有快速 GetToken 例程?它开始认为 AQTime 给出了误导性的结果,并得出结论认为它没有。

4

5 回答 5

26

AQTime 是一个检测分析器。检测分析器通常不适合测量代码时间,尤其是在像您这样的微基准测试中,因为检测的成本通常超过被测量的成本。另一方面,检测分析器擅长分析内存和其他资源使用情况。

定期检查 CPU 位置的采样分析器通常更适合测量代码时间。

无论如何,这是另一个微基准,它确实表明case语句比CharInSet. 但是,请注意集合检查仍然可以与类型转换一起使用以消除截断警告(实际上这是 CharInSet 存在的唯一原因):

{$apptype console}

uses Windows, SysUtils;

const
  SampleString = 'foo bar baz blah de;blah de blah.';

procedure P1;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while not CharInSet(cp^, [#0, ';', '.']) do
    Inc(cp);
end;

procedure P2;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    case cp^ of
      '.', #0, ';':
        Break;
    else
      Inc(cp);
    end;
end;

procedure P3;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while not (AnsiChar(cp^) in [#0, ';', '.']) do
    Inc(cp);
end;

procedure Time(const Title: string; Proc: TProc);
var
  i: Integer;
  start, finish, freq: Int64;
begin
  QueryPerformanceCounter(start);
  for i := 1 to 1000000 do
    Proc;
  QueryPerformanceCounter(finish);
  QueryPerformanceFrequency(freq);
  Writeln(Format('%20s: %.3f seconds', [Title, (finish - start) / freq]));
end;

begin
  Time('CharInSet', P1);
  Time('case stmt', P2);
  Time('set test', P3);
end.

它在我的笔记本电脑上的输出是:

CharInSet: 0.261 seconds
case stmt: 0.077 seconds
 set test: 0.060 seconds
于 2008-12-02T03:42:35.237 回答
8

Barry,我想指出,您的基准测试并不能反映各种方法的实际性能,因为实现的结构不同。相反,所有方法都应使用“while True do”构造,以更好地反映执行 char-in-set 检查的不同方法的影响。

这里替换了测试方法(P2 未更改,P1 和 P3 现在使用“while True do”构造):

procedure P1;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    if CharInSet(cp^, [#0, ';', '.']) then
      Break
    else
      Inc(cp);
end;

procedure P2;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    case cp^ of
      '.', #0, ';':
        Break;
    else
      Inc(cp);
    end;
end;

procedure P3;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    if AnsiChar(cp^) in [#0, ';', '.'] then
      Break
    else
      Inc(cp);
end;

我的工作站提供:

CharInSet: 0.099 seconds
case stmt: 0.043 seconds
 set test: 0.043 seconds

哪个更符合预期结果。对我来说,似乎使用“case in”构造并没有真正帮助。对不起马可!

于 2008-12-02T09:55:53.877 回答
3

Delphi 的免费采样分析器可以在这里找到:

https://forums.codegear.com/thread.jspa?messageID=18506

除了仪器分析器的时间测量不正确的问题之外,应该注意的是,哪个更快还取决于“案例”分支的可预测性。如果“case”中的测试都遇到相似的概率,“case”的性能最终可能低于 CharInSet。

于 2008-12-11T13:54:05.387 回答
2

函数“CharInSet”中的代码比“case”快,时间花在“call”上,使用while not(cp^ in [..])然后

你会看到这是禁食。

于 2009-01-05T11:50:45.077 回答
1

据我所知,如果调用都使用短指针,则调用与跳转所需的处理器操作数量相同。与长指针可能不同。默认情况下,汇编程序中的调用不使用堆栈。如果有足够的空闲寄存器使用寄存器。所以堆栈操作也需要零时间。它只是非常快的寄存器。

相比之下,我看到的情况变体使用 add 和 sub 操作,这些操作非常慢,可能会增加大部分额外时间。

于 2008-12-02T03:46:43.107 回答