提问



我正在读一本书,作者说if( a < 901 )if( a <= 900 )快。


与此简单示例不完全相同,但循环复杂代码略有性能变化。我想这必须对生成的机器代码做一些事情,以防它甚至是真的。

最佳参考


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



  • testcmp指令,设置EFLAGS

  • Jcc(跳转)指令,取决于比较类型(和代码布局):

    • jne - 如果不相等则跳转 - > ZF = 0

    • jz - 如果为零(等于) - > ZF = 1
    • 则跳转
    • jg - 如果更大则跳跃 - > ZF = 0 and SF = OF

    • (等...)







示例(编辑简洁)编译$ gcc -m32 -S -masm=intel test.c [63]


    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指令。这两个将花费相同的时间。





我想说的是没有任何东西表明不同的跳转指令需要花费相同的时间。这个问题有点难以回答,但是我能给出的是:在英特尔指令集参考中,它们是所有在一条共同指令下组合在一起,Jcc(如果条件满足则跳转)。相同的分组在优化参考手册的附录C中进行了说明。延迟和吞吐量。[64] [65]



  延迟 - 所需的时钟周期数
  执行核心来完成所有形成的μops的执行
  一个指令。

  
  吞吐量 - 所需的时钟周期数
  在问题端口可以自由接受相同的指令之前等待
  再次。对于许多指令,指令的吞吐量可以是
  显着低于其延迟



Jcc的值是:


      Latency   Throughput
Jcc     N/A        0.5


关于Jcc的以下脚注:



  7)条件跳转指令的选择应基于第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

其它参考1


从历史上看(我们说的是20世纪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到内存速度比的时代相关,但它现在几乎完全无关紧要。

其它参考2


假设我们在谈论内部整数类型,那么没有可能比另一种更快。它们显然在语义上是相同的。它们都要求编译器做同样的事情。只有一个可怕的破坏编译器会为其中一个生成劣质代码。


如果某个平台的<<=快于简单整数类型,则编译器总是将<=转换为<的常量。任何编译器都不会是一个糟糕的编译器(对于那个平台)。

其它参考3


我发现两者都不快。编译器在每个条件下使用不同的值生成相同的机器代码。


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

其它参考4


对于浮点代码,即使在现代架构上,< =比较可能确实更慢(通过一条指令)。这是第一个功能:


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需要评估为假。

其它参考5


也许这本未命名的书的作者已经读到a > 0a >= 1运行得更快并认为这是普遍的。


但这是因为0涉及(因为CMP可以,取决于架构,例如用OR替换而不是因为<

其它参考6


至少,如果这是真的,编译器可以轻而易举地优化<=b到!(a> b),所以即使比较本身实际上更慢,除了最天真的编译器之外你不会注意到区别。

其它参考7


他们有相同的速度。也许在某些特殊的架构中他/她说的是对的,但至少在x86家族中我知道它们是相同的。因为为此,CPU将进行减法(a-b),然后检查标志寄存器的标志。该寄存器的两位称为ZF(零标志)和SF(符号标志),它在一个周期内完成,因为它将通过一个屏蔽操作完成。

其它参考8


这将高度依赖于C编译的底层架构。某些处理器和体系结构可能具有等于或小于等于在不同循环数中执行的明确指令。


这可能是非常不寻常的,因为编译器可以解决它,使其无关紧要。

其它参考9


其他答案集中在x86架构上,我不知道ARM架构(你的示例汇编器似乎是这样)足以对生成的代码做出具体评论,但这是一个微优化的例子是非常具体的架构,并且很可能是一个反优化,因为它是一个优化[66] [67] [68]


因此,我建议这种微优化是货物崇拜编程的一个例子,而不是最好的软件工程实践。[69] [70]


可能一些架构这是一个优化,但我知道至少有一个架构,其中可能相反。令人尊敬的Transputer架构只有等于和大于或等于的机器代码指令,因此所有比较都必须从这些原语构建。[71]


即便如此,在几乎所有情况下,编译器都可以按照这样的方式对评估指令进行排序:在实践中,没有任何比较比任何其他比较都有任何优势。但最糟糕的情况是,可能需要添加反向指令(REV)来交换操作数堆栈中的前两项。这是一个单字节指令,需要一个周期才能运行,所以可能的开销最小。[72]


这样的微优化是优化还是反优化取决于您使用的特定架构,因此进入这个通常是一个坏主意使用特定于体系结构的微观优化的习惯,否则你可能本能地使用一个不适合这样做,并且看起来这正是你正在阅读的书所倡导的。

其它参考10


即使有任何差异,您也不应该注意到差异。此外,在实践中,你将不得不做一个额外的a + 1a - 1来使条件成立,除非你使用一些魔法常数,这是一种非常糟糕的做法。

其它参考11


您可以说大多数脚本语言中的行是正确的,因为额外的字符会导致代码处理稍慢。
 但是,正如最佳答案所指出的那样,它在C ++中应该没有任何效果,而使用脚本语言完成的任何事情都可能并不关心优化。

其它参考12


实际上,它们的速度完全相同,因为在装配级别它们都采用一条线。如:



  • jl ax,dx(如果AX小于DX则跳转)

  • jle ax,dx(如果AX小于或等于DX则跳转)



所以不,也不是更快。但是如果你想获得真正的技术,我想如果你要在电子电流水平检查它,它会稍微快一点,但不会接近你会注意到的速度。