我听过一位老师放弃了这一次,从那以后一直困扰着我。假设我们要检查整数x
是否大于或等于0.有两种方法可以检查:
if (x > -1){
//do stuff
}
和
if (x >= 0){
//do stuff
}
根据这位老师>
,会比>=
略快一些。在这种情况下它是Java,但据他说,这也适用于C,c ++和其他语言。这句话有什么道理吗?
答案 0 :(得分:29)
它非常依赖于底层架构,但任何差异都是微不足道的。
如果有的话,我希望(x >= 0)
稍快一些,因为与0
的比较在某些指令集(例如ARM)上免费提供。
当然,任何合理的编译器都会选择最佳实现,无论您的源代码是哪种变体。
答案 1 :(得分:29)
在任何现实世界的意义上都没有区别。
让我们看看各种编译器为各种目标生成的一些代码。
-O2
,MSVC为/Ox
,IAR为-Oh
)使用以下模块:
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|
MSVC 11定位x64:
// 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
MSVC 11定位x86:
// 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
GCC 4.6.1定位x64
// if (x > -1) {...
cmp_gt:
.seh_endprologue
test ecx, ecx
js .L2
// if (x >= 0) {...
cmp_gte:
.seh_endprologue
test ecx, ecx
js .L5
GCC 4.6.1定位x86:
// 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
GCC 4.4.1针对ARM:
// if (x > -1) {...
cmp_gt:
.fnstart
.LFB0:
cmp r0, #0
blt .L8
// if (x >= 0) {...
cmp_gte:
.fnstart
.LFB1:
cmp r0, #0
blt .L2
针对ARM Cortex-M3的IAR 5.20:
// 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中累积的这些表达式的性能差异所花费的时间。如果您有能力衡量性能差异,请将您的努力应用于更重要的事情,例如研究亚原子粒子的行为等。
答案 2 :(得分:19)
你的老师一直在读一些很旧的书。过去一些架构缺少greater than or equal
指令,评估>
所需的机器周期比>=
少,但这些平台现在很少见。我建议使用>= 0
来获取可读性。
答案 3 :(得分:13)
这里更大的问题是premature optimisation。许多人认为编写可读代码比编写高效代码[1,2]更重要。一旦设计被证明有效,我会将这些优化应用为低级库中的最后一个阶段。
您不应该一直考虑以可读性为代价在代码中进行微小的优化,因为它会使读取和维护代码变得更加困难。如果需要进行这些优化,请将它们抽象为较低级别的函数,这样您仍然可以使用更容易为人类阅读的代码。
作为一个疯狂的例子,考虑一下将他们的程序编写成一个愿意放弃额外效率并使用Java在设计,易用性和可维护性方面获益的人。
作为旁注,如果你正在使用C,或许编写一个使用稍微高效的代码的宏是一个更可行的解决方案,因为它将比分散操作更高效,可读性和可维护性。
当然,效率和可读性的权衡取决于您的应用。如果该循环每秒运行10000次,那么它可能是一个瓶颈,你可能想花时间去优化它,但如果它是一个单独的声明,偶尔会调用它可能不值得为它获得分钟。
答案 4 :(得分:9)
是的,有区别,你应该看到字节码。
代表
if (x >= 0) {
}
字节码是
ILOAD 1
IFLT L1
代表
if (x > -1) {
}
字节码是
ILOAD 1
ICONST_M1
IF_ICMPLE L3
版本1更快,因为它使用特殊的零操作数操作
iflt : jump if less than zero
但是有可能看到仅在仅解释模式java -Xint ...
中运行JVM的区别,例如此测试
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 ms,对于n = 1显示760 ms。(我使用1代替-1,因为它更容易演示,这个想法保持不变)
答案 5 :(得分:4)
事实上,我认为第二个版本应该稍微快一点,因为它需要一个比特检查(假设您在上面显示时比较为零)。然而,这种优化从未真正表现出来,因为大多数编译器都会优化这些调用。
答案 6 :(得分:3)
“&gt; =”是单一操作,就像“&gt;”一样。不是与OR进行2次单独操作。
但是&gt; = 0可能更快,因为计算机只需要检查一位(负号)。
答案 7 :(得分:1)
根据这位老师&gt;会稍微快于&gt; =。在这 例如它是Java,但据他说这也适用于C,c ++ 和其他语言。这句话有什么道理吗?
你的老师根本就错了。 不仅为什么机会比与0比较可以很快,但因为这种局部优化很好地由你的编译器/解释器完成,你可以把所有尝试帮助的东西弄得一团糟。绝对不是一件好事。
答案 8 :(得分:1)
很抱歉打断这个关于表现的对话。
在我离题之前,让我们注意JVM有特殊的instructions,不仅可以处理零,还可以处理一到三的常量。有了这个说法,很可能架构处理零的能力远远超过了编译器优化,而且还有字节码到机器代码的翻译等等。
我记得在我的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
这些替代方案花费相同的时间,因为指令是相同或相似的,并且它们消耗可预测的时钟周期数。例如,请参阅this。 ja
和jae
可能确实检查了不同数量的算术寄存器,但该检查主要是指令需要采取可预测的时间。这反过来又需要保持CPU架构的可管理性。
但我来这里是为了离题。
在我之前的答案往往是恰当的,并且表明无论你选择哪种方法,你都会在表演方面处于相同的范围内。
这使您可以根据其他标准进行选择。这就是我想做笔记的地方。在测试索引时,更喜欢紧密绑定样式检查(主要是x >= lowerBound
)到x > lowerBound - 1
。这个论点必然是人为的,但它归结为可读性,因为这里所有其他的都是平等的。
从概念上讲,你是在测试下限,x >= lowerBound
是规范测试,可以从你的代码读者那里获得最适应的认知。 x + 10 > lowerBound + 9
,x - lowerBound >= 0
和x > -1
都是测试下限的迂回方式。
再次,抱歉进入,但我觉得这比学术界的事情更重要。我一直在思考这些术语,让编译器担心它认为可以摆脱常量和操作符严格性的微小优化。
答案 9 :(得分:0)
首先,它高度依赖于硬件平台。 对于现代PC和ARM SoC,差异主要依赖于编译器优化。 但对于没有FPU的CPU,签名数学将是灾难。
例如,简单的8位CPU,如Intel 8008,8048,8051,Zilog Z80,Motorola 6800,甚至是现代RISC PIC或Atmel微型控制器,通过ALU和8位寄存器完成所有数学计算,基本上只带有标志位和z(零值指示符)标志位。所有严肃的数学都是通过库和表达式
完成的 BYTE x;
if (x >= 0)
肯定会赢,使用JZ或JNZ asm指令,而无需非常昂贵的库调用。
答案 10 :(得分:0)
这取决于底层架构。带有 Jazzelle 的旧 ARMv6 能够直接执行 Java 字节码。否则,字节码被翻译成机器码。有时,目标平台需要消耗额外的机器周期来创建操作数 -1
或 0
,但另一个可能会在解码比较指令时加载它们。其他的,例如 OpenRISC 定义了一个寄存器,它一直保持为 0,可以进行比较。 很可能 某些平台很少需要从较慢的内存中加载操作数。
综上所述,操作符的速度不是Java编程语言所规定的,泛化一个特定的案例就违背了使用跨平台编程语言的目的。