< 比 <= 更快吗?

1770

if (a < 901)if (a <= 900)快吗?

在这个简单的例子中不完全如此,但是在循环复杂代码中会有轻微的性能差异。我认为如果这是真的,它与生成的机器代码有关。


191
我认为这个问题没有被关闭的理由(尤其是不应该被删除,因为当前的投票结果显示它有历史意义、回答质量高,并且其他 [tag:performance] 中的热门问题仍然保持着开放状态)。最多只需要锁定。即使这个问题本身存在误解/天真,但它出现在一本书中的事实意味着原始的错误信息在某些"可信"来源中存在,因此这个问题是有建设性的,它有助于澄清这一点。 - Jason C
34
你从未告诉我们你指的是哪一本书。 - Jonathon Reinhart
216
< 比打 <= 快两倍。 - Deqing
10
在 8086 上是正确的。 - Joshua
13
点赞的数量明显显示出有数百人在过度优化。 - m93a
显示剩余6条评论
16个回答

1838

不,大多数架构上都不会更快。你没有指定,但在x86上,所有整数比较通常都会实现为两个机器指令:

  • 一个testcmp指令,设置EFLAGS
  • 和一个Jcc(跳转)指令,取决于比较类型(和代码布局):
  • jne - 如果不相等则跳转 --> ZF = 0
  • jz - 如果为零(相等)则跳转 --> ZF = 1
  • jg - 如果大于则跳转 --> ZF = 0 and SF = OF
  • (等等...)

示例(编辑后简化)使用$ gcc -m32 -S -masm=intel test.c编译

    if (a < b) {
        // Do something 1
    }

编译为:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

    if (a <= b) {
        // Do something 2
    }

编译为:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

因此,两者之间唯一的区别在于jgjge指令。这两个指令需要相同的时间。


I'd like to address the comment that nothing indicates that the different jump instructions take the same amount of time. This one is a little tricky to answer, but here's what I can give: In the Intel Instruction Set Reference, they are all grouped together under one common instruction, Jcc (Jump if condition is met). The same grouping is made together under the Optimization Reference Manual, in Appendix C. Latency and Throughput.

延迟 - 执行核心需要的时钟周期数,以完成构成指令的所有μops的执行。

吞吐量 - 等待发行端口再次可用以接受相同指令所需的时钟周期数。对于许多指令,其吞吐量可能显着低于其延迟。

Jcc 的值为:

      Latency   Throughput
Jcc     N/A        0.5

以下是关于Jcc的脚注:
选择条件跳转指令应基于第3.4.1节“分支预测优化”的建议,以提高分支的可预测性。当分支成功预测时,jcc的延迟实际上为零。
因此,在英特尔文档中,没有任何内容将一个Jcc指令与其他指令区别对待。
如果考虑用于实现指令的实际电路,可以假设在EFLAGS的不同位上有简单的AND/OR门,以确定是否满足条件。然后,测试两个位的指令不必比测试一个位的指令花费更多或更少的时间(忽略门传播延迟,这远小于时钟周期)。

编辑:浮点数

对于x87浮点数同样适用:(与上面几乎相同的代码,但使用double而不是int。)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

248
@Dyppl 实际上 jgjnle 是相同的指令,都是 7F :-) - Jonathon Reinhart
24
更不用说,如果优化器确实发现其中一种选项更快,它可以修改代码。 - Elazar Leibovich
4
仅仅因为某个东西产生了相同数量的指令,并不意味着执行所有这些指令所需的总时间将是相同的。实际上,更多的指令可能会更快地执行。每个周期的指令并不是一个固定的数字,它取决于指令本身。 - jontejj
25
我非常清楚这一点。你读了我的回答吗?我没有提到任何关于指令数量相同的事情,我说它们被编译成基本上完全相同的指令,除了一个跳转指令查看一个标志,另一个跳转指令查看两个标志。我相信我已经给出了足够的证据来表明它们在语义上是相同的。 - Jonathon Reinhart
5
@jontejj,你提出了一个非常好的观点。由于这个答案获得了很高的关注度,我应该对它进行一些清理。感谢你的反馈。 - Jonathon Reinhart
显示剩余16条评论

622

历史上(我们指的是80年代和90年代初),有一些架构满足这个条件。根本问题在于,整数比较本质上是通过整数减法来实现的。这会导致以下情况。

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

A < B 时,减法需要借位才能得到正确的结果,就像手算加减法一样需要进位和借位。这个“借位”的位通常被称为进位标志位,并且可以通过分支指令测试。第二个位叫做零标志位,如果减法的结果恰好为零,则会设置该位,这意味着相等。

通常至少有两个条件分支指令,一个用于根据进位标志位进行分支,另一个用于根据零标志位进行分支。

现在,为了深入了解问题的核心,让我们扩展之前的表格以包括进位和零位的结果。

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

因此,针对 A < B 的分支可以在一条指令中完成,因为进位标志位仅在这种情况下被清除,也就是说,

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

但是,如果我们想进行小于或等于比较,则需要对零标志进行额外检查以捕获相等的情况。

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

因此,在某些机器上,使用“小于”比较可能会节省一条机器指令。这在亚兆赫处理器速度和1:1的CPU到内存速度比之时是相关的,但在今天几乎完全不相关。


11
此外,像x86这样的架构实现了诸如jge的指令,该指令测试零标志位和符号/进位标志位。 - greyfade
4
即使对于给定的架构,也有可能编译器的开发人员从未注意到它,并添加了一种优化来将较慢的替换为更快的。请问这种情况的可能性有多大? - Jon Hanna
9
在8080处理器上,这是事实。它有跳转到零和跳转到负数的指令,但没有同时测试两者的指令。 - user597225
5
这也适用于6502和65816处理器家族,这一扩展也适用于Motorola 68HC11/12。 - Lucas
38
即使在8080上,使用交换操作数并测试“not <”(相当于“> =”)就可以用一条指令实现<=测试。这是交换操作数的所需<=cmp B,A; bcs addr。这就是Intel省略此测试的原因,他们认为它是多余的,并且在那些时代你无法承受冗余的指令 :-) - Gunther Piez
显示剩余11条评论

98
假设我们谈论的是内部整数类型,那么一个比另一个更快是不可能的。它们在语义上显然是相同的。它们都要求编译器做完全相同的事情。只有一个极其错误的编译器才会为其中一个生成劣质代码。
如果存在一些平台,对于简单的整数类型,"<"比"<="更快,那么编译器应该始终将"<="转换为"<"以获取常量。任何没有这样做的编译器都只是一个糟糕的编译器(对于该平台而言)。

8
+1 我同意。直到编译器决定它们的速度之前,既不是 < 也不是 <= 会具有速度。当考虑到编译器通常已经执行死代码优化、尾递归优化、循环提升(以及在某些情况下展开循环)、各种循环的自动并行化等时,这对编译器来说是非常简单的优化。为什么要浪费时间思考过早的优化?先跑一个原型,用分析工具确定最显著的优化位置,按重要性顺序执行这些优化,并在执行过程中再次使用分析工具进行测量,以了解进展情况... - autistic
仍有一些边缘情况,其中一个常量值的比较在<=下可能会变慢,例如当从(a < C)转换为(a <= C-1)(对于某个常量C)导致在指令集中更难编码C时。例如,指令集可以以紧凑形式表示来自-127到128的有符号常量的比较,但是超出该范围的常量必须使用更长,更慢的编码或另一个完全不同的指令来加载。因此,像(a < -127)这样的比较可能没有直接的转换方式。 - BeeOnRope
@BeeOnRope问题不在于因为具有不同常数而执行不同操作会影响性能,而是表达相同操作时使用不同常数是否会影响性能。因此,我们不会将a > 127a> 128进行比较,因为您在那里没有选择,您使用所需的内容。我们正在比较a> 127a>= 128,因为它们具有相同的真值表,所以不能需要不同的编码或不同的指令。对一个进行编码同样也是对另一个进行编码。 - David Schwartz
我是在一般性地回应你的陈述:“如果有某个平台[<=比<慢],编译器应该总是将<=转换为<来处理常量”。据我所知,这种转换涉及更改常量。例如,a <= 42被编译为a < 43,因为<更快。在某些边缘情况下,这样的转换可能不会产生成果,因为新常量可能需要更多或更慢的指令。当然,a > 127a >= 128是等效的,编译器应该以(相同的)最快方式编码两种形式,但这并不与我所说的不一致。 - BeeOnRope

68

我看到两者都没有更快的情况。编译器会生成相同的机器代码,只是使用不同的值。

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

我的示例if来自于Linux平台上的x86_64架构的GCC编译器。

编译器的作者都很聪明,他们会考虑到我们大多数人所忽略的许多细节。

我注意到,如果它不是一个常量,那么在任何情况下生成的机器代码都是相同的。

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

10
请注意,这是针对x86架构的。 - Michael Petrotta
11
我认为您应该使用if(a <=900)来演示它生成完全相同的汇编代码 :) - Lipis
2
@AdrianCornish 对不起..我编辑了一下..它更或多或少是相同的..但是如果你把第二个if改成<=900,那么汇编代码将完全相同:)现在基本上是一样的..但你知道..为了强迫症患者 :) - Lipis
3
那可能会被简化为 if (true) 并完全消除。 - Qsario
5
没有人指出这种优化只适用于常量比较。我可以保证,对于比较两个变量,不会像这样进行优化。 - Jonathon Reinhart
显示剩余17条评论

54

对于浮点数代码来说,即使在现代架构上,<= 比较可能确实会慢一些(多一个指令)。以下是第一个函数:

int compare_strict(double a, double b) { return a < b; }

在PowerPC上,首先进行浮点数比较(更新条件寄存器cr),然后将条件寄存器移动到通用寄存器(GPR)中,将“比较小于”位移到指定位置,最后返回。这需要四个指令。

现在考虑以下这个函数:

int compare_loose(double a, double b) { return a <= b; }

这需要与上面的compare_strict相同的工作,但现在有两个有趣的位:“小于”和“等于”。这需要使用一个额外的指令(cror-条件寄存器按位或)将这两个位组合成一个。因此compare_loose需要五条指令,而compare_strict需要四条。

您可能认为编译器可以像这样优化第二个函数:

int compare_loose(double a, double b) { return ! (a > b); }

然而,这样会错误地处理NaN。NaN1 <= NaN2NaN1 > NaN2都需要评估为false。


幸运的是,在x86(x87)上它不会像这样工作。fucomip设置ZF和CF。 - Jonathon Reinhart
5
我认为你误解了PowerPC的操作-条件寄存器cr等同于x86上的标志位,如ZFCF。(尽管CR更灵活。)发帖者所说的是将结果移动到GPR:这需要两个PowerPC指令,而x86具有条件移动指令。 - Dietrich Epp
@DietrichEpp 我在我的陈述之后想要补充的是:你可以根据 EFLAGS 的值立即跳转。抱歉没有表达清楚。 - Jonathon Reinhart
1
@JonathonReinhart:是的,你还可以根据CR的值立即跳转。答案并没有谈论跳转,这就是额外指令的来源。 - Dietrich Epp

34

或许那本未提及名字的书的作者读到了 a > 0a >= 1 运行得更快,认为这是普遍真实的。

但事实上,这是因为涉及到一个 0(因为根据架构不同,CMP 可以被替换为例如 OR),而不是因为 <


1
当然,在“调试”构建中,但是除非编译器有问题,否则(a >= 1)的运行速度不会比(a > 0)慢,因为优化器可以将前者轻松转换为后者。 - BeeOnRope
2
@BeeOnRope 有时我会惊讶于优化器能够优化的复杂事物,以及它在那些简单的事物上无法做到优化。 - glglgl
1
确实,对于极少数需要关注的函数,检查汇编输出总是值得的。话虽如此,上述转换非常基础,甚至在简单的编译器中也已经执行了几十年。 - BeeOnRope

30

至少可以这样说,如果这是真的,编译器可以将 a <= b 轻松地优化为 !(a > b),因此即使比较本身实际上更慢,除了最简单的编译器之外,您也不会注意到差异。


为什么 !(a>b) 是 a<=b 的优化版本。难道 !(a>b) 不是两个操作吗? - Abhishek Singh
6
@AbhishekSingh,“NOT”只是由其他指令(如“je”与“jne”)组成的。 - Pavel Gatnar
在IEEE浮点数中,“a<=b”并不意味着与“!(a>b)”相同。 - Don Hatch

18

TL;DR 答案:

对于大多数架构、编译器和语言组合,<不会比<=更快。

完整回答:

其他答案已经着重讨论了x86架构,而我对ARM架构(似乎是您的汇编程序使用的)并不熟悉,无法具体评论所生成代码,但这是一个非常特定于架构的微优化示例,可能正好相反,即是一种反优化。

因此,我建议这种微优化实际上是一种模拟崇拜式编程,而不是最佳软件工程实践。

反例:

可能有一些架构可以进行优化,但我知道至少有一种架构,相反情况可能是真实的。古老的Transputer架构只有机器码指令等于大于或等于,所以所有比较都必须从这些基本操作构建。

即使在几乎所有情况下,编译器都可以按照某种方式对评估指令进行排序,因此实际上没有任何比较优于其他任何比较。但最差的情况是,它可能需要添加一个反转指令(REV)来交换操作数栈上的前两个项目。这是一个单字节指令,需要一个周期运行,所以开销最小。
总结: 这样的微观优化是一种优化还是反优化取决于您使用的特定架构,因此通常不应养成使用特定于架构的微观优化的习惯,否则当不适合使用时您可能会本能地使用其中之一,而且看起来这正是您正在阅读的书籍提倡的。

15

它们的速度是相同的。也许在某些特殊的架构下他/她所说的是正确的,但至少在x86系列中,我知道它们是相同的。因为为了做这件事,CPU 将进行一个减法运算 (a - b) ,然后检查标志寄存器的标志。该寄存器的两个比特被称为 ZF (零标志位) 和 SF (符号标志位),并且它在一个周期内完成,因为它将使用一次掩码操作来完成。


14

这将高度依赖于C编译的底层架构。有些处理器和架构可能具有明确的等于或小于等于指令,其执行的周期数也不同。

然而这种情况相当罕见,因为编译器可以解决这个问题,使其变得无关紧要。


2
如果循环中存在差异,1)它将无法检测到。2)任何值得一试的编译器都会在不改变代码含义的情况下从慢速形式转换为更快的形式。因此,生成的指令将是相同的。 - Martin York
完全同意,这将是一个相当琐碎和愚蠢的区别。毫无疑问,在任何情况下都不值得在应该是平台无关的书中提及。 - Telgin
@lttlrck:我明白了,花了我一段时间(我真傻)。它们不可检测,因为有很多其他事情发生,使它们的测量变得不可能。处理器停顿/缓存未命中/信号/进程交换。因此,在正常的操作系统情况下,单周期级别的东西不能被物理测量。如果您可以消除所有干扰测量(在具有板载内存和无操作系统的芯片上运行),那么您仍然需要担心计时器的粒度,但从理论上讲,如果您运行足够长的时间,您可以看到一些东西。 - Martin York

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接