协慌网

登录 贡献 社区

< 快于 <=?

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

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

答案

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

  • 设置EFLAGS testcmp指令
  • 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指令。这两个人将花费相同的时间。


我想解决的问题是,没有任何迹象表明不同的跳转指令需要相同的时间。这个问题有点难以回答,但这是我能给出的:在英特尔指令集参考中 ,它们都在一个通用指令Jcc (满足条件时跳转)下组合在一起。在优化参考手册的附录 C 中,相同的分组一起进行。延迟和吞吐量。

延迟 - 执行内核完成执行构成指令的所有μop 所需的时钟周期数。

吞吐量 - 在发出端口可以再次接受相同指令之前等待所需的时钟周期数。对于许多指令,指令的吞吐量可能远远小于其延迟

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

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

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

如果对于简单整数类型存在<快于<=平台,则编译器应始终<=转换为<以获取常量。任何编译器都不会只是一个糟糕的编译器(对于该平台)。