【问题标题】:Micro-optimizing a c++ comparison function微优化 c++ 比较函数
【发布时间】:2013-03-24 02:20:58
【问题描述】:

我有一个看起来像这样的Compare() 函数:

inline bool Compare(bool greater, int p1, int p2) {
  if (greater) return p1>=p2;
  else return p1<=p2;
}

我决定优化以避免分支:

inline bool Compare2(bool greater, int p1, int p2) {
  bool ret[2] = {p1<=p2,p1>=p2};
  return ret[greater];
}

然后我通过这样做进行了测试:

bool x = true;
int M = 100000;
int N = 100;

bool a[N];
int b[N];
int c[N];

for (int i=0;i<N; ++i) {
  a[i] = rand()%2;
  b[i] = rand()%128;
  c[i] = rand()%128;
}

// Timed the below loop with both Compare() and Compare2()
for (int j=0; j<M; ++j) {
  for (int i=0; i<N; ++i) {
    x ^= Compare(a[i],b[i],c[i]);
  }
}

结果:

Compare(): 3.14ns avg
Compare2(): 1.61ns avg

我会说案例关闭,避免分支 FTW。但是为了完整起见,我替换了

a[i] = rand()%2;

与:

a[i] = true;

得到了完全相同的测量结果,约为 3.14ns。据推测,那时没有进行分支,编译器实际上正在重写Compare() 以避免if 语句。但是,为什么Compare2() 更快呢?

不幸的是,我是汇编代码文盲,否则我会尝试自己回答这个问题。

编辑:下面是一些程序集:

_Z7Comparebii:
.LFB4:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -8(%rbp)
    movl    %edx, -12(%rbp)
    movb    %al, -4(%rbp)
    cmpb    $0, -4(%rbp)
    je      .L2
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setge   %al
    jmp     .L3
.L2:
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setle   %al
.L3:
    leave
    ret
    .cfi_endproc
.LFE4:
    .size   _Z7Comparebii, .-_Z7Comparebii
    .section        .text._Z8Compare2bii,"axG",@progbits,_Z8Compare2bii,comdat
    .weak   _Z8Compare2bii
    .type   _Z8Compare2bii, @function
_Z8Compare2bii:
.LFB5:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -24(%rbp)
    movl    %edx, -28(%rbp)
    movb    %al, -20(%rbp)
    movw    $0, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setle   %al
    movb    %al, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setge   %al
    movb    %al, -15(%rbp)
    movzbl  -20(%rbp), %eax
    cltq
    movzbl  -16(%rbp,%rax), %eax
    leave
    ret
    .cfi_endproc
.LFE5:
    .size   _Z8Compare2bii, .-_Z8Compare2bii
    .text

现在,执行测试的实际代码可能正在使用上述两个函数的内联版本,因此这可能是要分析的错误代码。话虽如此,我在Compare() 中看到了一个jmp 命令,所以我认为这意味着它正在分支。如果是这样,我想这个问题就变成了:当我将a[i]rand()%2 更改为true(或false)时,为什么分支预测器不能提高Compare() 的性能?

EDIT2:我将“分支预测”替换为“分支”以使我的帖子更合理。

【问题讨论】:

  • optimize to avoid branch prediction这不是矛盾吗?
  • 您必须共享汇编代码,因为发生的情况很大程度上取决于您使用的编译器以及优化级别。
  • @Last Line:那你为什么不发布程序集呢?
  • 你没有设置种子。也许编译器足够聪明,知道rand() 在这种情况下返回什么?只是一个快速的想法。另外,您应该真正比较程序集。即使你是汇编代码文盲,你仍然可以展示差异。
  • 可能是有条件的移动.. 显示程序集。

标签: c++ optimization branch-prediction


【解决方案1】:

我想我想出了大部分。

当我在我的 OP 编辑​​中发布函数的程序集时,我注意到内联版本可能不同。我没有检查或发布时序代码,因为它比较复杂,并且因为我认为内联过程不会改变分支是否发生在Compare()

当我取消内联函数并重复我的测量时,我得到了以下结果:

Compare(): 7.18ns avg
Compare2(): 3.15ns avg

然后,当我将a[i]=rand()%2 替换为a[i]=false 时,我得到了以下信息:

Compare(): 2.59ns avg
Compare2(): 3.16ns avg

这证明了分支预测的好处。 a[i] 替换没有产生任何改进的事实最初表明内联删除了分支。

所以最后一个谜团是为什么内联的Compare2() 优于内联的Compare()。我想我可以发布时序代码的程序集。函数如何内联的一些怪癖可能会导致这种情况似乎很合理,所以我很乐意在这里结束我的调查。我将在我的应用程序中将Compare() 替换为Compare2()

感谢许多有用的 cmets。

编辑:我应该补充一点,Compare2 击败所有其他人的可能原因是处理器能够并行执行这两个比较。正是这种直觉让我按照自己的方式编写函数。所有其他变体本质上都需要两个逻辑串行操作。

【讨论】:

    【解决方案2】:

    我编写了一个名为 Celero 的 C++ 库,旨在测试此类优化和替代方案。 (无耻的自我宣传:https://github.com/DigitalInBlue/Celero

    我使用以下代码运行您的案例:

    class StackOverflowFixture : public celero::TestFixture
    {
      public:
        StackOverflowFixture()
        {
        }
    
        inline bool NoOp(bool greater, int p1, int p2) 
        {
          return true;
        }
    
        inline bool Compare(bool greater, int p1, int p2) 
        {
          if(greater == true)
          {
            return p1>=p2;
          }
    
          return p1<=p2;
        }
    
        inline bool Compare2(bool greater, int p1, int p2)
        {
          bool ret[2] = {p1<=p2,p1>=p2};
          return ret[greater];
        }
    
        inline bool Compare3(bool greater, int p1, int p2) 
        {
          return (!greater != !(p1 <= p2)) | (p1 == p2);
        }
    
        inline bool Compare4(bool greater, int p1, int p2) 
        {
          return (greater ^ (p1 <= p2)) | (p1 == p2);
        }
    };
    
    BASELINE_F(StackOverflow, Baseline, StackOverflowFixture, 100, 5000000)
    {
      celero::DoNotOptimizeAway(NoOp(rand()%2, rand(), rand()));
    }
    
    BENCHMARK_F(StackOverflow, Compare, StackOverflowFixture, 100, 5000000)
    {
      celero::DoNotOptimizeAway(Compare(rand()%2, rand(), rand()));
    }
    
    BENCHMARK_F(StackOverflow, Compare2, StackOverflowFixture, 100, 5000000)
    {
      celero::DoNotOptimizeAway(Compare2(rand()%2, rand(), rand()));
    }
    
    BENCHMARK_F(StackOverflow, Compare3, StackOverflowFixture, 100, 5000000)
    {
      celero::DoNotOptimizeAway(Compare3(rand()%2, rand(), rand()));
    }
    
    BENCHMARK_F(StackOverflow, Compare4, StackOverflowFixture, 100, 5000000)
    {
      celero::DoNotOptimizeAway(Compare4(rand()%2, rand(), rand()));
    }
    

    结果如下:

    [==========]
    [  CELERO  ]
    [==========]
    [ STAGE    ] Baselining
    [==========]
    [ RUN      ] StackOverflow.Baseline -- 100 samples, 5000000 calls per run.
    [     DONE ] StackOverflow.Baseline  (0.690499 sec) [5000000 calls in 690499 usec] [0.138100 us/call] [7241140.103027 calls/sec]
    [==========]
    [ STAGE    ] Benchmarking
    [==========]
    [ RUN      ] StackOverflow.Compare -- 100 samples, 5000000 calls per run.
    [     DONE ] StackOverflow.Compare  (0.782818 sec) [5000000 calls in 782818 usec] [0.156564 us/call] [6387180.672902 calls/sec]
    [ BASELINE ] StackOverflow.Compare 1.133699
    [ RUN      ] StackOverflow.Compare2 -- 100 samples, 5000000 calls per run.
    [     DONE ] StackOverflow.Compare2  (0.700767 sec) [5000000 calls in 700767 usec] [0.140153 us/call] [7135039.178500 calls/sec]
    [ BASELINE ] StackOverflow.Compare2 1.014870
    [ RUN      ] StackOverflow.Compare3 -- 100 samples, 5000000 calls per run.
    [     DONE ] StackOverflow.Compare3  (0.709471 sec) [5000000 calls in 709471 usec] [0.141894 us/call] [7047504.408214 calls/sec]
    [ BASELINE ] StackOverflow.Compare3 1.027476
    [ RUN      ] StackOverflow.Compare4 -- 100 samples, 5000000 calls per run.
    [     DONE ] StackOverflow.Compare4  (0.712940 sec) [5000000 calls in 712940 usec] [0.142588 us/call] [7013212.893091 calls/sec]
    [ BASELINE ] StackOverflow.Compare4 1.032500
    [==========]
    [ COMPLETE ]
    [==========]
    

    鉴于此测试,看起来 Compare2 是此微优化的最佳选择。

    编辑:

    Compare2 组装(最好的情况):

    cmp r8d, r9d
    movzx   eax, dl
    setle   BYTE PTR ret$[rsp]
    cmp r8d, r9d
    setge   BYTE PTR ret$[rsp+1]
    movzx   eax, BYTE PTR ret$[rsp+rax]
    

    Compare3 组装(次佳案例):

    xor r11d, r11d
    cmp r8d, r9d
    mov r10d, r11d
    setg    r10b
    test    dl, dl
    mov ecx, r11d
    sete    cl
    mov eax, r11d
    cmp ecx, r10d
    setne   al
    cmp r8d, r9d
    sete    r11b
    or  eax, r11d
    

    【讨论】:

    • 有趣,但在这里我们想知道为什么
    • 我在回复中添加了程序集。
    • 我不喜欢你是如何进行基准测试的。测量的时间主要由 rand() 的成本决定,掩盖了变体之间的真实性能差异。
    • 确实 rand() 很昂贵,但每个测试的成本相同,因此可以将其分解。应该比较的是基线(相对)时间。这显示了真正更快的速度和速度。测量平均执行时间实际上是不正确的。参考:codeproject.com/Articles/525576/…
    • 给定基线,Compare2 比基线测量慢 1.014870 倍,Compare3 慢 1.027476 倍。
    【解决方案3】:

    这个怎么样...

    inline bool Compare3(bool greater, int p1, int p2) 
    {
      return (!greater != !(p1 <= p2)) | (p1 == p2);
    }
    

    inline bool Compare4(bool greater, int p1, int p2) 
    {
      return (greater ^ (p1 <= p2)) | (p1 == p2);
    }
    

    【讨论】:

    • 在我看来Compare3(true,1,1)!=Compare3(false,1,1),这会使函数不正确。 Compare4() 也一样。
    • 添加 | (p1 == p2) 并快乐。
    • 嗯,我没有测试代码。我的家用机器上没有编译器。现在会检查。
    • 该死,我错过了那个条件。现在修好了。谢谢。
    • 这并没有真正解决问题(即“为什么 Compare() 和 Compare2() 之间有区别?”)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多