if (a < 901)
比if (a <= 900)
快吗?
在这个简单的例子中不完全如此,但是在循环复杂代码中会有轻微的性能差异。我认为如果这是真的,它与生成的机器代码有关。
if (a < 901)
比if (a <= 900)
快吗?
在这个简单的例子中不完全如此,但是在循环复杂代码中会有轻微的性能差异。我认为如果这是真的,它与生成的机器代码有关。
不,大多数架构上都不会更快。你没有指定,但在x86上,所有整数比较通常都会实现为两个机器指令:
test
或cmp
指令,设置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:
因此,两者之间唯一的区别在于jg
和jge
指令。这两个指令需要相同的时间。
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
的脚注: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
jg
和 jnle
是相同的指令,都是 7F
:-) - Jonathon Reinhart历史上(我们指的是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到内存速度比之时是相关的,但在今天几乎完全不相关。
jge
的指令,该指令测试零标志位和符号/进位标志位。 - greyfade<=
测试。这是交换操作数的所需<=
: cmp B,A; bcs addr
。这就是Intel省略此测试的原因,他们认为它是多余的,并且在那些时代你无法承受冗余的指令 :-) - Gunther Piez<
也不是 <=
会具有速度。当考虑到编译器通常已经执行死代码优化、尾递归优化、循环提升(以及在某些情况下展开循环)、各种循环的自动并行化等时,这对编译器来说是非常简单的优化。为什么要浪费时间思考过早的优化?先跑一个原型,用分析工具确定最显著的优化位置,按重要性顺序执行这些优化,并在执行过程中再次使用分析工具进行测量,以了解进展情况... - autistic(a < C)
转换为(a <= C-1)
(对于某个常量C
)导致在指令集中更难编码C
时。例如,指令集可以以紧凑形式表示来自-127到128的有符号常量的比较,但是超出该范围的常量必须使用更长,更慢的编码或另一个完全不同的指令来加载。因此,像(a < -127)
这样的比较可能没有直接的转换方式。 - BeeOnRopea > 127
与a> 128
进行比较,因为您在那里没有选择,您使用所需的内容。我们正在比较a> 127
与a>= 128
,因为它们具有相同的真值表,所以不能需要不同的编码或不同的指令。对一个进行编码同样也是对另一个进行编码。 - David Schwartz<=
转换为<
来处理常量”。据我所知,这种转换涉及更改常量。例如,a <= 42
被编译为a < 43
,因为<
更快。在某些边缘情况下,这样的转换可能不会产生成果,因为新常量可能需要更多或更慢的指令。当然,a > 127
和a >= 128
是等效的,编译器应该以(相同的)最快方式编码两种形式,但这并不与我所说的不一致。 - BeeOnRope我看到两者都没有更快的情况。编译器会生成相同的机器代码,只是使用不同的值。
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
if(a <=900)
来演示它生成完全相同的汇编代码 :) - Lipis对于浮点数代码来说,即使在现代架构上,<= 比较可能确实会慢一些(多一个指令)。以下是第一个函数:
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 <= NaN2
和NaN1 > NaN2
都需要评估为false。
fucomip
设置ZF和CF。 - Jonathon Reinhartcr
等同于x86上的标志位,如ZF
和CF
。(尽管CR更灵活。)发帖者所说的是将结果移动到GPR:这需要两个PowerPC指令,而x86具有条件移动指令。 - Dietrich Epp或许那本未提及名字的书的作者读到了 a > 0
比 a >= 1
运行得更快,认为这是普遍真实的。
但事实上,这是因为涉及到一个 0
(因为根据架构不同,CMP
可以被替换为例如 OR
),而不是因为 <
。
(a >= 1)
的运行速度不会比(a > 0)
慢,因为优化器可以将前者轻松转换为后者。 - BeeOnRope至少可以这样说,如果这是真的,编译器可以将 a <= b 轻松地优化为 !(a > b),因此即使比较本身实际上更慢,除了最简单的编译器之外,您也不会注意到差异。
对于大多数架构、编译器和语言组合,<
不会比<=
更快。
其他答案已经着重讨论了x86架构,而我对ARM架构(似乎是您的汇编程序使用的)并不熟悉,无法具体评论所生成代码,但这是一个非常特定于架构的微优化示例,可能正好相反,即是一种反优化。
因此,我建议这种微优化实际上是一种模拟崇拜式编程,而不是最佳软件工程实践。
可能有一些架构可以进行优化,但我知道至少有一种架构,相反情况可能是真实的。古老的Transputer架构只有机器码指令等于和大于或等于,所以所有比较都必须从这些基本操作构建。
即使在几乎所有情况下,编译器都可以按照某种方式对评估指令进行排序,因此实际上没有任何比较优于其他任何比较。但最差的情况是,它可能需要添加一个反转指令(REV)来交换操作数栈上的前两个项目。这是一个单字节指令,需要一个周期运行,所以开销最小。它们的速度是相同的。也许在某些特殊的架构下他/她所说的是正确的,但至少在x86系列中,我知道它们是相同的。因为为了做这件事,CPU 将进行一个减法运算 (a - b) ,然后检查标志寄存器的标志。该寄存器的两个比特被称为 ZF (零标志位) 和 SF (符号标志位),并且它在一个周期内完成,因为它将使用一次掩码操作来完成。
这将高度依赖于C编译的底层架构。有些处理器和架构可能具有明确的等于或小于等于指令,其执行的周期数也不同。
然而这种情况相当罕见,因为编译器可以解决这个问题,使其变得无关紧要。
<
比打<=
快两倍。 - Deqing