【问题标题】:is i=(i+1)&3 faster than i=(i+1)%4i=(i+1)&3 比 i=(i+1)%4 快吗
【发布时间】:2012-01-13 14:13:36
【问题描述】:

我正在优化 C++ 代码。 在一个关键步骤,我想实现以下功能y=f(x)

f(0)=1

f(1)=2

f(2)=3

f(3)=0

哪个更快?使用查找表或i=(i+1)&3i=(i+1)%4?还是有更好的建议?

【问题讨论】:

  • 你为什么不尝试两者并测量?另请参阅:stackoverflow.com/questions/8316332/…
  • 最好的方法是测量。检查生成的代码可能会告诉您它们是相等的。如果您真的想要沉迷于微优化:避免使用该函数并将其设为宏或内联函数。
  • 谢谢大家。所有的 cmets 都是正确的并且非常有用。
  • 回答那些问我为什么不同时尝试这两种方法的人:我希望有人可以提供一个我没有想到的开箱即用的解决方案。无论如何,谢谢。

标签: c++ micro-optimization


【解决方案1】:

几乎可以肯定,查找表将是最慢的。在很多情况下,编译器会为(i+1)&3(i+1)%4 生成相同的程序集;但是,根据 i 的类型/签名,它们可能不是严格等效的,编译器将无法进行该优化。以代码为例

int foo(int i)
{
    return (i+1)%4;
}

unsigned bar(unsigned i)
{
    return (i+1)%4;
}

在我的系统上,gcc -O2 生成:

0000000000000000 <foo>:
   0:   8d 47 01                lea    0x1(%rdi),%eax
   3:   89 c2                   mov    %eax,%edx
   5:   c1 fa 1f                sar    $0x1f,%edx
   8:   c1 ea 1e                shr    $0x1e,%edx
   b:   01 d0                   add    %edx,%eax
   d:   83 e0 03                and    $0x3,%eax
  10:   29 d0                   sub    %edx,%eax
  12:   c3                      retq   

0000000000000020 <bar>:
  20:   8d 47 01                lea    0x1(%rdi),%eax
  23:   83 e0 03                and    $0x3,%eax
  26:   c3                      retq

如您所见,由于有符号模数结果的规则,(i+1)%4 首先会生成更多代码。

归根结底,如果 (i+1)&amp;3 版本能表达你想要的,你可能最好使用它,因为编译器做你不期望的事情的机会更少。

【讨论】:

    【解决方案2】:

    我不会讨论过早优化。但答案是它们的速度是一样的。

    任何理智的编译器都会将它们编译成相同的东西。无论如何,除以 2 的幂的除法/模数将优化为按位运算。

    所以使用你找到的(或其他人会找到的)更易读的。

    编辑:正如 Roland 所指出的,它有时会根据符号的不同而表现不同:

    无符号&:

    int main(void)
    {
        unsigned x;
        cin >> x;
        x = (x + 1) & 3;
        cout << x;
    
        return 0;
    }
    
    mov eax, DWORD PTR _x$[ebp]
    inc eax
    and eax, 3
    push    eax
    

    无符号模数:

    int main(void)
    {
        unsigned x;
        cin >> x;
        x = (x + 1) % 4;
        cout << x;
    
        return 0;
    }
    
    mov eax, DWORD PTR _x$[ebp]
    inc eax
    and eax, 3
    push    eax
    

    签名&:

    int main(void)
    {
        int x;
        cin >> x;
        x = (x + 1) & 3;
        cout << x;
    
        return 0;
    }
    
    mov eax, DWORD PTR _x$[ebp]
    inc eax
    and eax, 3
    push    eax
    

    有符号模数:

    int main(void)
    {
        int x;
        cin >> x;
        x = (x + 1) % 4;
        cout << x;
    
        return 0;
    }
    
    mov eax, DWORD PTR _x$[ebp]
    inc eax
    and eax, -2147483645            ; 80000003H
    jns SHORT $LN3@main
    dec eax
    or  eax, -4                 ; fffffffcH
    

    【讨论】:

    • 我认为没有任何编译器足够聪明,无法将查找表转换为与模数/与相同的代码。请参阅我的回答,了解为什么 %&amp; 版本可能会生成不同的代码。
    • 我忘了提到查找表,但是是的,编译器不太可能能够优化它。
    【解决方案3】:

    很有可能,您不会发现任何差异:任何合理的现代编译器都知道将两者优化为相同的代码。

    【讨论】:

      【解决方案4】:

      您是否尝试过对其进行基准测试?顺便说一下,我假设&amp;3 版本会更快,因为这是一个简单的加法和按位与运算,这两者都应该是任何现代 CPU 上的单周期运算。

      %4 可以采用几种不同的方式,具体取决于编译器的智能程度。它可以通过除法来完成,这比加法要慢得多,或者也可以转换为按位的and 运算,最终与&amp;3 版本一样快。

      【讨论】:

        【解决方案5】:

        与 Mystical 相同,但 C 和 ARM

        int fun1 ( int i )
        {
            return( (i+1)&3 );
        }
        
        int fun2 ( int i )
        {
            return( (i+1)%4 );
        }
        
        unsigned int fun3 ( unsigned int i )
        {
            return( (i+1)&3 );
        }
        
        unsigned int fun4 ( unsigned int i )
        {
            return( (i+1)%4 );
        }
        

        创建:

        00000000 <fun1>:
           0:   e2800001    add r0, r0, #1
           4:   e2000003    and r0, r0, #3
           8:   e12fff1e    bx  lr
        
        0000000c <fun2>:
           c:   e2802001    add r2, r0, #1
          10:   e1a0cfc2    asr ip, r2, #31
          14:   e1a03f2c    lsr r3, ip, #30
          18:   e0821003    add r1, r2, r3
          1c:   e2010003    and r0, r1, #3
          20:   e0630000    rsb r0, r3, r0
          24:   e12fff1e    bx  lr
        
        00000028 <fun3>:
          28:   e2800001    add r0, r0, #1
          2c:   e2000003    and r0, r0, #3
          30:   e12fff1e    bx  lr
        
        00000034 <fun4>:
          34:   e2800001    add r0, r0, #1
          38:   e2000003    and r0, r0, #3
              3c:   e12fff1e    bx  lr
        

        对于负数,掩码和模数不相等,仅适用于正数/无符号数。对于这些情况,您的编译器应该知道 %4 与 &3 相同,并在 (&3) 上使用较便宜的 gcc 。下面是clang/llc

        fun3:                             
            add r0, r0, #1
            and r0, r0, #3
            mov pc, lr
        
        fun4:
            add r0, r0, #1
            and r0, r0, #3
            mov pc, lr
        

        【讨论】:

          【解决方案6】:

          当然 & 比 % 更快。以前的许多帖子都证明了这一点。此外,由于 i 是局部变量,您可以使用 ++i 而不是 i+1,因为大多数编译器都可以更好地实现它。 i+1 可能(不)被优化为 ++i。

          更新:也许我不清楚,我的意思是,函数应该只是“return((++i)&3);”

          【讨论】:

          • -1, i=(++i)&amp;3 实际上是无效的(两次写入,两者都不是在另一个之前排序)。
          • 我认为你解释错了...我试图告诉 return((++i)&3))。
          猜你喜欢
          • 2012-08-25
          • 2015-05-24
          • 2015-08-17
          • 2014-02-22
          • 2014-10-23
          • 2022-01-13
          • 1970-01-01
          • 2020-05-24
          • 1970-01-01
          相关资源
          最近更新 更多