【问题标题】:In C++, which is faster? (2 * i + 1) or (i << 1 | 1)?在 C++ 中,哪个更快? (2 * i + 1) 还是 (i << 1 | 1)?
【发布时间】:2010-12-07 04:44:45
【问题描述】:

我意识到答案可能是特定于硬件的,但我很好奇我是否缺少更一般的直觉?

我问了this 问题并给出了答案,现在我想知道我是否应该改变我的方法以使用“(i

【问题讨论】:

  • 我不确定,但它可能适用于相同的机器指令......所以我会说选择更具可读性的那个。
  • @Jon Seigel:“可读”意味着更清楚地表达了代码的意图。你(OP)是乘以二加一,还是向左移动并设置 LSB?
  • 您正在尝试做编译器会做的工作。所以你最好不要。^^
  • 我发现第一个版本的阅读速度更快。第二个版本需要考虑一下您要实现的目标。结果我总是使用第一个As it is the fastest to understand

标签: c++ assembly performance bit-shift


【解决方案1】:

由于 ISO 标准实际上并没有强制要求性能要求,这将取决于实现、选择的编译器标志、目标 CPU 以及很可能的月相。

与算法选择等宏观层面的优化相比,这类优化(节省几个周期)在投资回报方面几乎总是显得微不足道。

首先以代码的可读性为目标。如果您的意图是移动位和OR,请使用位移位版本。如果您的意图是增加,请使用* 版本。仅在确定存在问题后才担心性能。

任何体面的编译器都会比你优化得更好:-)

【讨论】:

  • 希望编译器不依赖于月相,但现在我想起来了,我已经使用了一些似乎确实依赖于潮汐特征的东西?
  • 因为涨潮时会被淹?我可能会建议将服务器移到更高的高度... ;)
  • 编译器未能使用位移/加法来优化乘法,这让我非常失望。
  • @Knoblauch 你有介绍过性能吗?也许使用乘法允许 CPU 微码使用 SIMD/SSE2 指令来比位移位更快?
  • 更不用说前面的说明了。许多处理器可以并行执行多个操作,但不能执行多个相同类型的操作。因此,如果前一个操作是位移位,则使用实数乘法是有意义的。你甚至可以得到 a *= 2; b*= 2 使用两种不同的操作的违反直觉的结果,正是因为它们是不同的!
【解决方案2】:

只是一个关于“......它将使用LEA”的答案的实验:
以下代码:

int main(int argc, char **argv)
{
#ifdef USE_SHIFTOR
return (argc << 1 | 1);
#else
return (2 * argc + 1);
#endif
}

将带有gcc -fomit-frame-pointer -O8 -m{32|64}(适用于32位或64位)编译成以下汇编代码:

  1. x86, 32bit:
    080483a0 
    : 80483a0: 8b 44 24 04 移动 0x4(%esp),%eax 80483a4: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax 80483a8:c3 ret
  2. x86, 64bit:
    00000000004004c0 
    : 4004c0: 8d 44 3f 01 lea 0x1(%rdi,%rdi,1),%eax 4004c4: c3 retq
  3. x86, 64bit, -DUSE_SHIFTOR:
    080483a0 
    : 80483a0: 8b 44 24 04 移动 0x4(%esp),%eax 80483a4: 01 c0 添加 %eax,%eax 80483a6: 83 c8 01 或 $0x1,%eax 80483a9:c3 ret
  4. x86, 32bit, -DUSE_SHIFTOR:
    00000000004004c0 
    : 4004c0: 8d 04 3f lea (%rdi,%rdi,1),%eax 4004c3: 83 c8 01 或 $0x1,%eax 4004c6: c3 retq

事实上,大多数情况下确实会使用LEA。然而,这两种情况的代码相同。有两个原因:

  1. 加法可以溢出和回绕,而像&lt;&lt;| 这样的位运算则不能
  2. (x + 1) == (x | 1) 只有在 !(x &amp; 1) 时才为真,否则加法会延续到下一位。一般来说,加一只会导致在一半的情况下设置最低位。

虽然我们(可能还有编译器)知道第二个必然适用,但第一个仍然是可能的。因此,编译器会创建不同的代码,因为“or-version”需要将位 0 ​​强制为 1。

【讨论】:

  • gcc (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5
  • 很高兴看到有人确实将猜测和疯狂的假设进行了测试。但是您解释为什么 gcc 不优化 shift 版本是错误的:您的第 1 点无效,x
  • @drhirsch:立场更正 ;-) 你是对的,完成了测试,gcc 4.7.2 为 32/64 位创建相同的代码,无论源代码的确切公式如何。
【解决方案3】:

除了最脑残的编译器之外,任何编译器都会将这些表达式视为等价的,并将它们编译为相同的可执行代码。

通常情况下,优化这些简单的算术表达式并不值得过多担心,因为这是编译器最擅长优化的事情。 (与许多其他“智能编译器”可以做正确的事情,但实际编译器却一败涂地的情况不同。)

顺便说一句,这将适用于 PPC、Sparc 和 MIPS 上的同一对指令:移位后加。在 ARM 上,它会变成一条融合的移位加法指令,而在 x86 上,它可能是一条 LEA 操作。

【讨论】:

  • 这不能在 x86 上编译成单个 LEA 吗?
  • 是的,大概是LEA EAX, EAX + EAX + 1x86下的最快方式。
【解决方案4】:

带有 -S 选项的 gcc 输出(没有给出编译器标志):

.LCFI3:
        movl    8(%ebp), %eax
        addl    %eax, %eax
        orl     $1, %eax
        popl    %ebp
        ret

.LCFI1:
        movl    8(%ebp), %eax
        addl    %eax, %eax
        addl    $1, %eax
        popl    %ebp
        ret

我不确定哪个是哪个,但我认为这并不重要。

如果编译器根本不进行优化,那么第二个可能会转换为更快的汇编指令。每条指令需要多长时间完全取决于架构。大多数编译器会将它们优化为相同的汇编级指令。

【讨论】:

  • 实际上,您不能说,一般来说,第二个将是最快的,因为很可能有一个架构,其中添加的速度是移位速度的十倍 (不太可能,但我的观点是它依赖于平台)。如果您将自己限制在特定平台上,可能是这种情况,但您可能应该在答案中明确说明。
  • 记住一句谚语:没有 -O3 的基准测试就像比较 F1 车手在滑板上的速度。
【解决方案5】:

我刚刚用FrankH的源码用gcc-4.7.1测试了这个,生成的代码是

lea    0x1(%rdi,%rdi,1),%eax
retq

无论是使用移位还是乘法版本。

【讨论】:

    【解决方案6】:

    没有人关心。他们也不应该。
    不必为此担心,让您的代码正确、简单并完成。

    【讨论】:

    • 我们可以不那么消极,或者至少通过说“编译器将同等对待这两种形式”来支持你的说法吗?
    • 好的,好的,抱歉。 “如果你关心这个细节的速度,你应该可能会编写手工制作的汇编程序”怎么样?不?一般来说,在编写 cpp 时,我力求正确、简单和完成。如果优化不是从简单性出发,那么你只是在乞求下一个可怜的懒汉拿起这段代码来追捕你并射杀你......
    【解决方案7】:

    i + i + 1 可能比其他两个更快, 因为加法比乘法快,而且比移位快。

    【讨论】:

    • 这个答案没有帮助,因为它是一个毫无根据的猜测,甚至没有任何分析或反汇编的暗示来支持它。它鼓励人们“微优化”,正如其他答案所说,这是错误的。
    【解决方案8】:

    较快的是第一种形式(右移的形式),实际上 shr 指令在最坏的情况下需要 4 个时钟周期才能完成,而 mul 在最好的情况下需要 10 个时钟周期。但是,最好的形式应该由编译器决定,因为它可以完整地查看其他(汇编)指令。

    【讨论】:

      猜你喜欢
      • 2015-05-24
      • 1970-01-01
      • 2020-05-24
      • 2012-01-13
      • 2020-12-28
      • 1970-01-01
      • 2015-08-04
      • 2015-08-17
      • 2022-07-13
      相关资源
      最近更新 更多