if(a<901)是否比if(a<=900)快?

与这个简单示例中的情况不同,但循环复杂代码的性能略有变化。我想这需要对生成的机器代码进行一些处理,以防它是真的。


当前回答

在C和C++中,编译器的一个重要规则是“仿佛”规则:如果执行X的行为与执行Y的行为完全相同,那么编译器可以自由选择使用哪一个。

在您的情况下,“a<901”和“a<=900”总是具有相同的结果,因此编译器可以自由编译任一版本。无论出于什么原因,如果一个版本更快,那么任何高质量的编译器都会为更快的版本生成代码。因此,除非您的编译器生成异常糟糕的代码,否则两个版本的运行速度将相等。

现在,如果你有一种情况,两位代码总是会产生相同的结果,但编译器很难证明,和/或编译器很难证实哪个版本更快,那么你可能会得到不同的代码以不同的速度运行。

PS如果处理器支持单字节常量(更快)和多字节常量(更慢),则原始示例可能以不同的速度运行,因此与255(1字节)进行比较可能比与256(2字节)进行对比更快。我希望编译器做任何更快的事情。

其他回答

TL;DR答案

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

完整答案

其他答案都集中在x86架构上,我不太了解ARM架构(您的示例汇编程序似乎是这样),无法对生成的代码进行具体评论,但这是一个非常特定于架构的微优化示例,很可能是反优化,也可能是优化。

因此,我认为这种微优化是货物崇拜编程的一个例子,而不是最佳软件工程实践。

反例

可能有一些架构是优化的,但我知道至少有一种架构可能是相反的。古老的Transputer体系结构只有等于和大于或等于的机器代码指令,因此所有比较都必须从这些原语构建。

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

总结

像这样的微优化是优化还是反优化取决于您正在使用的特定架构,因此养成使用特定架构的微优化的习惯通常是一个坏主意,否则您可能会在不合适时本能地使用微优化,看起来这正是您正在阅读的书所倡导的。

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

测试或cmp指令,用于设置EFLAGS以及Jcc(跳转)指令,具体取决于比较类型(和代码布局):jne-如果不等于-->ZF=0,则跳转jz-如果零(等于)-->ZF=1则跳转jg-如果大于-->ZF=0且SF=OF,则跳转(等等)


示例(为简洁起见编辑)使用$gcc-m32-S-masm=inteltest.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:

And

    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:

因此,两者之间的唯一区别是jg与jge指令。这两者将花费相同的时间。


我想指出的是,没有任何东西表明不同的跳转指令需要相同的时间。这个问题回答起来有点棘手,但我可以给出以下答案:在“英特尔指令集参考”中,它们都被分组在一条公共指令Jcc(如果满足条件则跳转)下。根据附录C中的《优化参考手册》进行了相同的分组。延迟和吞吐量。

Latency—执行核心,以完成形成的所有μ操作的执行指令。

吞吐量—需要的时钟周期数等待发出端口可以接受相同的指令再一次对于许多指令,指令的吞吐量可以是显著低于其延迟

Jcc的值为:

      Latency   Throughput
Jcc     N/A        0.5

Jcc上有以下脚注:

条件跳转指令的选择应基于第3.4.1节“分支预测优化”的建议,以提高分支的可预测性。当成功预测分支时,jcc的延迟实际上为零。

因此,在英特尔文档中,对一条Jcc指令的处理方式与其他指令没有任何区别。

如果考虑用于实现指令的实际电路,可以假设EFLAGS中的不同位上有简单的“与/或”门,以确定是否满足条件。因此,没有理由让一条测试两个位的指令比只测试一个位花费更多或更少的时间(忽略比时钟周期短得多的门传播延迟)


编辑:浮点

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

您可以说这行在大多数脚本语言中都是正确的,因为额外的字符会导致代码处理稍慢。然而,正如上面的答案所指出的,它在C++中应该没有任何影响,而使用脚本语言所做的任何事情都可能不太关心优化。

从历史上看(我们所说的是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与内存速度比的时代是相关的,但在今天几乎完全不相关。

仅当计算路径依赖于数据时:

a={1,1,1,1,1000,1,1,1,1}
while (i<=4)
{
     for(j from 0 to a[i]){ do_work(); }
     i++;
}

将计算250倍以上的时间(i<4)

真实世界的例子是计算曼德布洛特集合。如果包含一个迭代1000000次的像素,它将导致延迟,但与<=使用概率的重合度太低。