【问题标题】:Why is CharInSet faster than Case statement?为什么 CharInSet 比 Case 语句快?
【发布时间】:2010-09-24 21:07:23
【问题描述】:

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

4,894,539 次处决:

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

时间为 0.25 秒。

但执行次数相同:

而真正做到
案例 P^ 的
' ', #10, #13, #0: 休息;
其他公司(P);
结束;

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

CharInSet 语句的汇编代码是:

添加edi,$02
mov edx,$0064b290
movzx eax,[edi]
调用 CharInSet
测试 a1,a1
jz $00649f18(回到add语句)

而案例逻辑就是这样:

movzx eax,[edi]
子斧头,$01
jb $00649ef0
子斧头,09 美元
jz $00649ef0
子斧,$03
jz $00649ef0
添加 edi,$02
jmp $00649ed6(回到 movzx 语句)

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

function CharInSet(C: AnsiChar; const CharSet: TSysCharSet): Boolean;
开始
结果 := CharSet 中的 C;
结束;

我认为这样做的唯一原因是因为 [' ', #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(和其他检测分析器)进行错误优化的可能性。知道这一点将有助于我重新做出Profiler and Memory Analysis Tools for Delphi 的决定,这也是我的问题How Does AQTime Do It? 的另一个答案。当然,AQTime 在检测时不会更改代码,所以它必须使用其他魔法来做到这一点。

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


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

【问题讨论】:

  • AQTime 在检测时确实会更改代码,这就是检测分析器的工作方式。
  • 与大多数分析器不同,AQTime 以某种方式在不修改源代码的情况下完成其工作。我真的很喜欢这样,因为不用担心失败会破坏你的代码。它必须以某种方式分析目标代码并将其链接回源代码行以进行分析。但我还是不知道它是怎么做到的。
  • 我想我知道它是如何做到的。见:stackoverflow.com/questions/322315/how-does-aqtime-do-it

标签: delphi delphi-2009 case-statement


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      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”构造并没有真正帮助。对不起,马可!

      【讨论】:

      • 在比较特定结构方面这是一个很好的观察。但是 Barry 的代码仍然可以比较可能的实现,这些实现应该与它们的编码方式进行比较。我想要最快的实现。
      • 在我看来,您似乎对故意限制 set-test 方法感兴趣,因为它看起来与 case 语句不相似?无论如何,直到今天我才看到这个答案,因为您从未对我的答案发表评论:)
      【解决方案4】:

      可以在此处找到免费的 Delphi 采样分析器:

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

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

      【讨论】:

      • 链接损坏(不是你的错:))
      【解决方案5】:

      函数"CharInSet"中的代码比'case'快,时间花在'call'上, 采用 而不是 (cp^ in [..]) 然后

      你会看到这是禁食。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-02
        • 1970-01-01
        • 2013-04-23
        相关资源
        最近更新 更多