有一次我听到一位老师说过这句话,从那以后它就一直困扰着我。假设我们想要检查整数x
是否大于或等于0。有两种方法可以检查:
if (x > -1){
//do stuff
}
和
if (x >= 0){
//do stuff
}
根据这位教师的说法,>
比>=
略微快一些。在这种情况下,他所指的是Java语言,但他认为这同样适用于C、C++和其他语言。这个说法是否有道理?
有一次我听到一位老师说过这句话,从那以后它就一直困扰着我。假设我们想要检查整数x
是否大于或等于0。有两种方法可以检查:
if (x > -1){
//do stuff
}
和
if (x >= 0){
//do stuff
}
根据这位教师的说法,>
比>=
略微快一些。在这种情况下,他所指的是Java语言,但他认为这同样适用于C、C++和其他语言。这个说法是否有道理?
这非常依赖于底层架构,但任何差异都将微乎其微。
如果有什么区别,我会期望 (x >= 0)
稍微快一点,因为在某些指令集上(例如ARM),与 0
进行比较是免费的。
当然,任何明智的编译器都将选择最佳实现,而不管源代码中使用哪个变体。
从任何实际意义上来说,它们没有区别。
让我们看一些由各种编译器为各种目标生成的代码。
-O2
for GCC, /Ox
for MSVC, -Oh
for IAR)using the following module:
void my_puts(char const* s);
void cmp_gt(int x)
{
if (x > -1) {
my_puts("non-negative");
}
else {
my_puts("negative");
}
}
void cmp_gte(int x)
{
if (x >= 0) {
my_puts("non-negative");
}
else {
my_puts("negative");
}
}
以下是它们各自针对比较操作所产生的结果:
面向 ARM 的 MSVC 11:
// if (x > -1) {...
00000 |cmp_gt| PROC
00000 f1b0 3fff cmp r0,#0xFFFFFFFF
00004 dd05 ble |$LN2@cmp_gt|
// if (x >= 0) {...
00024 |cmp_gte| PROC
00024 2800 cmp r0,#0
00026 db05 blt |$LN2@cmp_gte|
针对 x64 的 MSVC 11:
// if (x > -1) {...
cmp_gt PROC
00000 83 f9 ff cmp ecx, -1
00003 48 8d 0d 00 00 // speculative load of argument to my_puts()
00 00 lea rcx, OFFSET FLAT:$SG1359
0000a 7f 07 jg SHORT $LN5@cmp_gt
// if (x >= 0) {...
cmp_gte PROC
00000 85 c9 test ecx, ecx
00002 48 8d 0d 00 00 // speculative load of argument to my_puts()
00 00 lea rcx, OFFSET FLAT:$SG1367
00009 79 07 jns SHORT $LN5@cmp_gte
面向x86的MSVC 11:
// if (x > -1) {...
_cmp_gt PROC
00000 83 7c 24 04 ff cmp DWORD PTR _x$[esp-4], -1
00005 7e 0d jle SHORT $LN2@cmp_gt
// if (x >= 0) {...
_cmp_gte PROC
00000 83 7c 24 04 00 cmp DWORD PTR _x$[esp-4], 0
00005 7c 0d jl SHORT $LN2@cmp_gte
目标为x64的GCC 4.6.1
// if (x > -1) {...
cmp_gt:
.seh_endprologue
test ecx, ecx
js .L2
// if (x >= 0) {...
cmp_gte:
.seh_endprologue
test ecx, ecx
js .L5
面向 x86 平台的 GCC 4.6.1:
// if (x > -1) {...
_cmp_gt:
mov eax, DWORD PTR [esp+4]
test eax, eax
js L2
// if (x >= 0) {...
_cmp_gte:
mov edx, DWORD PTR [esp+4]
test edx, edx
js L5
针对ARM的GCC 4.4.1:
// if (x > -1) {...
cmp_gt:
.fnstart
.LFB0:
cmp r0, #0
blt .L8
// if (x >= 0) {...
cmp_gte:
.fnstart
.LFB1:
cmp r0, #0
blt .L2
使用IAR 5.20针对ARM Cortex-M3进行编程:
// if (x > -1) {...
cmp_gt:
80B5 PUSH {R7,LR}
.... LDR.N R1,??DataTable1 ;; `?<Constant "non-negative">`
0028 CMP R0,#+0
01D4 BMI.N ??cmp_gt_0
// if (x >= 0) {...
cmp_gte:
80B5 PUSH {R7,LR}
.... LDR.N R1,??DataTable1 ;; `?<Constant "non-negative">`
0028 CMP R0,#+0
01D4 BMI.N ??cmp_gte_0
(x > -1)
和 (x >= 0)
的任何显著区别:
cmp r0,#0xFFFFFFFF
进行(x > -1)
比较,而使用cmp r0,#0
进行(x >= 0)
的比较。第一个指令的操作码长度要长两个字节。我想这可能会增加一些额外时间,所以我们将其称为(x >= 0)
的优势。cmp ecx, -1
进行(x > -1)
比较,而使用test ecx, ecx
进行(x >= 0)
的比较。第一个指令的操作码长度要长一个字节。我想这可能会增加一些额外时间,所以我们将其称为(x >= 0)
的优势。请注意,GCC和IAR生成了相同的机器代码用于这两种比较(除了使用的寄存器可能不同)。因此根据此项调查,看起来(x >= 0)
有一点微小的“更快”的机会。但是,无论最小的操作码字节编码可能具有什么优势(我强调可能具有),它肯定会被其他因素完全掩盖。
我会很惊讶如果你发现 Java 或 C# 的 Jitted 输出有任何不同。即使是像8位 AVR这样非常小的目标,我也不认为你会找到任何值得注意的差异。
简而言之,不要担心这种微观优化。我认为我的写作在处理这些表达式的性能方面花费的时间已经比我一生中所有执行它们的CPU所花费的时间都多了。如果您有测量性能差异的能力,请将您的努力应用于更重要的事情,如研究亚原子粒子的行为等。
x
刚刚被计算出来,那么0
比较是免费的(至少在ARM上),而-1
比较则需要显式的额外指令。 - Graham Borlandx > -1
与x >= 0
完全相同对待(即,它们注意到这些表达式是等价的)。如果计算了x
,我会期望它们也会这样做-目前我没有系统来测试这个假设。另一方面,MSVC ARM编译器会略微不同地处理它们,我能够测试MS ARM编译器。如果计算了x
,它仍然会为-1和0测试执行显式比较(在进行计算后仍然有一个cmp r3,#0
或cmp r3,#0xffffffff
)。 - Michael Burr(x > -1)
和(x >= 0)
之间不应该有什么值得担心的差异。即使优化只修复了其中一个比较,添加的cmp
也几乎是任何程序需要特别注意删除的最后一件事情。 - Michael Burr你的老师一直在阅读一些很古老的书。以前有些架构缺乏大于等于
指令,在这些平台上,计算>
所需的机器周期比>=
少,但如今这些平台已经很少见了。我建议优先考虑可读性,使用>= 0
。
BRGE
(分支指令,如果大于或等于)和 BRSH
(分支指令,如果相同或更高),因此你看不到任何区别。 - Sergey Kalinichenko是的,有所不同,您应该查看字节码。
对于
if (x >= 0) {}
字节码是指
ILOAD 1
IFLT L1
for
if (x > -1) {}
字节码是指
ILOAD 1
ICONST_M1
IF_ICMPLE L3
版本1更快,因为它使用了一种特殊的零操作数运算。
iflt : jump if less than zero
但是只有在使用解释器模式运行JVM时,才能看到差异,例如这个测试:java -Xint ...
int n = 0;
for (;;) {
long t0 = System.currentTimeMillis();
int j = 0;
for (int i = 100000000; i >= n; i--) {
j++;
}
System.out.println(System.currentTimeMillis() - t0);
}
当 n = 0 时,显示时间为 690 毫秒,当 n = 1 时,显示时间为 760 毫秒。(我使用了 1 而不是 -1 是因为这样更容易演示,但思路是相同的)
">="是一种单独的运算,就像">"一样。而不是使用OR分开两个操作。
但是">=0"可能更快,因为计算机只需要检查一位(负号)。
"x
是如何获得它的值的(数据流分析)。编译器可能已经知道结果而无需检查任何内容。 - Bo Perssonx > -1
优化成机器可以高效执行的代码,那么在一些指令集架构(如 MIPS)上,使用 >= 0
可能会更快(例如 MIPS 中有一个 bgez $reg, target
指令,可以根据寄存器的符号位进行分支)。虽然这种方式更快,但并不会使比较本身在软件方面变得更快。所有简单的指令都具有 1 个周期的延迟,无论是 or
(独立位)还是 add
。 - Peter Cordes很抱歉打扰这场有关性能的对话。
在我偏离主题之前,让我们注意到JVM有特殊的指令,不仅用于处理零,还处理常数一至三。因此,这种能力可能已经被架构上的优化措施所淹没,除了编译器优化外,还包括字节码到机器码的转换等。
我记得在我的x86汇编语言时代,指令集中有大于(ja
)和大于或等于(jae
)的指令。您可以使用以下之一:
; x >= 0
mov ax, [x]
mov bx, 0
cmp ax, bx
jae above
; x > -1
mov ax, [x]
mov bx, -1
cmp ax, bx
ja above
ja
和jae
可能确实检查不同数量的算术寄存器,但该检查被指令需要花费可预测时间所支配。反过来,这是为了保持CPU架构的可管理性。x >= lowerBound
,而不是x > lowerBound - 1
。这个参数肯定是牵强的,但归根结底是可读性,因为在这里所有其他因素都是相等的。
从概念上讲,您正在针对一个下限进行测试,x >= lowerBound
是最能引起读者适应性认知的规范测试。 x + 10 > lowerBound + 9
,x - lowerBound >= 0
和x > -1
都是间接测试下限的方法。
再次抱歉打扰,但我觉得这超出了学术范畴的重要性。我总是用这些术语思考,并让编译器担心通过调整常量和运算符的严格性来获得微小的优化。
ja
和jae
是无符号的大于/大于等于。所有数字都是无符号的>=0,且所有数字都不是>-1U
。你需要使用jg
和jge
。此外,请注意,像大多数ISA一样,x86允许与立即数进行比较:cmp ax,0
。或者作为优化,test ax,ax
将FLAGS设置为与零进行比较的方式相同,但更短。使用CMP reg,0与OR reg,reg测试寄存器是否为零? - Peter Cordes首先,这高度取决于硬件平台。 对于现代个人电脑和ARM SoCs,差异大多依赖于编译器优化。 但对于没有FPU的CPU,有符号数学将是灾难。
例如,像英特尔 8008、8048、8051、Zilog Z80、 Motorola 6800 或甚至现代 RISC PIC 或 Atmel 微控制器等简单的 8 位 CPU,所有数学运算都通过带有 8 位寄存器的 ALU 进行,并且基本上只有进位标志位和 z(零值指示)标志位。所有严肃的数学都是通过库完成的,并且表达式。
BYTE x;
if (x >= 0)
使用JZ或JNZ汇编指令而不需要非常昂贵的库调用,肯定会获胜。
x
的类型是什么? - Jon Skeetx
是一个uint
类型,考虑一下这意味着什么... - Jon Skeet