我曾经听过一位老师放弃了这个,从那以后它就一直困扰着我。假设我们要检查整数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)
会稍微快一点,因为0
在某些指令集(例如 ARM)上是免费的。
当然,任何明智的编译器都会选择最佳实现,而不管您的源代码中有哪个变体。
在任何现实世界的意义上都没有区别。
让我们看一下各种编译器为各种目标生成的一些代码。
-O2
对于 GCC,/Ox
对于 MSVC,-Oh
对于 IAR)使用以下模块:
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
针对 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
for(x > -1)
与cmp r0,#0
for (x >= 0)
。第一条指令的操作码长了两个字节。我想这可能会引入一些额外的时间,所以我们称之为优势(x >= 0)
cmp ecx, -1
for(x > -1)
与test ecx, ecx
for (x >= 0)
。第一条指令的操作码长一个字节。我想这可能会引入一些额外的时间,所以我们称之为优势(x >= 0)
请注意,GCC 和 IAR 为两种比较生成了相同的机器代码(可能使用了哪个寄存器除外)。因此,根据这项调查,似乎(x >= 0)
“更快”的可能性微乎其微。但是,无论最小化更短的操作码字节编码可能具有(我强调可能具有)的任何优势,肯定会被其他因素完全掩盖。
如果您发现 Java 或 C# 的 jitted 输出有什么不同,我会感到惊讶。即使对于像 8 位 AVR 这样的非常小的目标,我怀疑你会发现任何不同之处。
总之,不用担心这个微优化。我认为我在这里写的时间已经超过了这些表达式在我有生之年执行它们的所有 CPU 上累积的性能差异所花费的时间。如果您有能力测量性能差异,请将您的精力投入到更重要的事情上,例如研究亚原子粒子的行为或其他事情。
你的老师一直在读一些非常古老的书。过去,某些架构缺乏greater than or equal
评估>
所需机器周期数少于的指令>=
,但如今这些平台很少见。我建议追求可读性,并使用>= 0
.
这里更大的问题是过早的优化。许多人认为编写可读代码比编写高效代码更重要[ 1 , 2 ]。一旦设计被证明有效,我会将这些优化作为低级库的最后阶段应用。
您不应该经常考虑以牺牲可读性为代价对代码进行微小的优化,因为这会使代码的阅读和维护变得更加困难。如果需要进行这些优化,请将它们抽象为较低级别的函数,这样您仍然可以获得更易于人类阅读的代码。
作为一个疯狂的例子,考虑一个以汇编形式编写程序的人,而另一个人愿意放弃额外的效率并使用 Java 来获得设计、易用性和可维护性方面的好处。
附带说明一下,如果您使用的是 C,也许编写一个使用效率稍高的代码的宏是一个更可行的解决方案,因为它比分散的操作更能实现效率、可读性和可维护性。
当然,效率和可读性的权衡取决于您的应用程序。如果该循环每秒运行 10000 次,那么它可能是一个瓶颈,您可能需要花费时间来优化它,但如果它是一个偶尔调用的语句,那么它可能不值得一分钟的收益。
是的,有区别,你应该看到字节码。
为了
if (x >= 0) {}
字节码是
ILOAD 1
IFLT L1
为了
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 的 2 个单独的操作。
但是 >=0 可能更快,因为计算机只需要检查一位(负号)。
很抱歉插话这个关于性能的谈话。
在我离题之前,让我们注意 JVM 有特殊的指令,不仅可以处理零,还可以处理常量 1 到 3。话虽如此,架构处理零的能力很可能已经远远落后于编译器优化,还有字节码到机器码的翻译等。
我记得在我的 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
都是针对下限进行测试的迂回方法。
再一次,很抱歉闯入,但我觉得这在学术之外很重要。我总是以这些术语思考,让编译器担心它认为可以摆脱对常量和运算符的严格性摆弄的微小优化。
首先,它高度依赖于硬件平台。对于现代 PC 和 ARM SoC,差异主要取决于编译器优化。但对于没有 FPU 的 CPU,签名数学将是一场灾难。
例如简单的 8 位 CPU,如 Intel 8008、8048、8051、Zilog Z80、摩托罗拉 6800 甚至现代 RISC PIC 或 Atmel 微控制器通过具有 8 位寄存器的 ALU 进行所有数学运算,并且基本上只有进位标志位和 z(零值指示符)标志位。所有严肃的数学都是通过库和表达式完成的
BYTE x;
if (x >= 0)
肯定会赢,使用 JZ 或 JNZ asm 指令而无需非常昂贵的库调用。
这取决于底层架构。带有 Jazzelle 的旧 ARMv6 能够直接执行 Java 字节码。否则,字节码被翻译成机器码。有时,目标平台需要消耗额外的机器周期来创建操作数-1
or 0
,但另一个可能会在比较指令被解码时加载它们。其他的,例如 OpenRISC 定义了一个持续保持 0 的寄存器,可以对其进行比较。大多数情况下,某些平台需要从较慢的内存中加载操作数。综上所述,Java 编程语言并没有规定算子的速度,概括一个特定的情况违背了使用跨平台编程语言的目的。