x > -1与x >= 0是否有性能差异?

39

有一次我听到一位老师说过这句话,从那以后它就一直困扰着我。假设我们想要检查整数x是否大于或等于0。有两种方法可以检查:

if (x > -1){
    //do stuff
}

if (x >= 0){
    //do stuff
} 

根据这位教师的说法,>>=略微快一些。在这种情况下,他所指的是Java语言,但他认为这同样适用于C、C++和其他语言。这个说法是否有道理?


12
x的类型是什么? - Jon Skeet
10
整数 x - Grant Thomas
5
如果x是一个uint类型,考虑一下这意味着什么... - Jon Skeet
5
对于无符号类型,这些表达式毫无意义:第一个永远不成立,第二个总是成立。 - James Kanze
1
可能是重复的问题:<比<=更快吗? - nawfal
显示剩余12条评论
11个回答

31

这非常依赖于底层架构,但任何差异都将微乎其微。

如果有什么区别,我会期望 (x >= 0) 稍微快一点,因为在某些指令集上(例如ARM),与 0 进行比较是免费的。

当然,任何明智的编译器都将选择最佳实现,而不管源代码中使用哪个变体。


+1. 事实上,0 的参与很可能比两个比较运算符本身的差异(如果有的话)更重要。 - Thilo
1
@Thilo 这在某些架构上可能是正确的(如果是这样,我期望编译器自己进行更改)。在另一些架构上(比如英特尔),这两个操作所需的时间完全相同。 - James Kanze
1
编辑以提到编译器将自动选择最佳选项。 - Graham Borland
1
同意;程序员不应该担心这个层面的细节,除非他们正在编写架构。 - Aram Kocharyan
1
我想补充一下为什么>=0比>-1更快。这是因为汇编__总是__与0进行比较。如果第二个值不为0,则第一个值将通过第二个值进行添加(或减去),之后可能进行的比较为e,lt,le,gt,ge,ne(相等,小于,小于等于,大于,大于等于,不等于)。当然,添加的加法/减法需要额外的CPU周期。 - Destrictor

30

从任何实际意义上来说,它们没有区别。

让我们看一些由各种编译器为各种目标生成的代码。

  • I'm assuming a signed int operation (which seem the intent of the OP)
  • I've limited by survey to C and to compilers that I have readily at hand (admittedly a pretty small sample - GCC, MSVC and IAR)
  • basic optimizations enabled (-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) 的任何显著区别:

  • 针对 ARM 的 MSVC 使用cmp r0,#0xFFFFFFFF进行(x > -1)比较,而使用cmp r0,#0进行(x >= 0)的比较。第一个指令的操作码长度要长两个字节。我想这可能会增加一些额外时间,所以我们将其称为(x >= 0)的优势。
  • 针对 x86 的 MSVC 使用cmp ecx, -1进行(x > -1)比较,而使用test ecx, ecx进行(x >= 0)的比较。第一个指令的操作码长度要长一个字节。我想这可能会增加一些额外时间,所以我们将其称为(x >= 0)的优势。

请注意,GCC和IAR生成了相同的机器代码用于这两种比较(除了使用的寄存器可能不同)。因此根据此项调查,看起来(x >= 0)有一点微小的“更快”的机会。但是,无论最小的操作码字节编码可能具有什么优势(我强调可能具有),它肯定会被其他因素完全掩盖。

我会很惊讶如果你发现 Java 或 C# 的 Jitted 输出有任何不同。即使是像8位 AVR这样非常小的目标,我也不认为你会找到任何值得注意的差异。

简而言之,不要担心这种微观优化。我认为我的写作在处理这些表达式的性能方面花费的时间已经比我一生中所有执行它们的CPU所花费的时间都多了。如果您有测量性能差异的能力,请将您的努力应用于更重要的事情,如研究亚原子粒子的行为等。


如果在比较之前需要计算x怎么办?例如,非常常见的--x? - qPCR4vir
这些代码片段并没有真正说明一个事实,即如果x刚刚被计算出来,那么0比较是免费的(至少在ARM上),而-1比较则需要显式的额外指令。 - Graham Borland
@GrahamBorland:请注意,这里的大多数ARM示例将x > -1x >= 0完全相同对待(即,它们注意到这些表达式是等价的)。如果计算了x,我会期望它们也会这样做-目前我没有系统来测试这个假设。另一方面,MSVC ARM编译器会略微不同地处理它们,我能够测试MS ARM编译器。如果计算了x,它仍然会为-1和0测试执行显式比较(在进行计算后仍然有一个cmp r3,#0cmp r3,#0xffffffff)。 - Michael Burr
@MichaelBurr,微软编译器无法发现这个明显的优化并不让我感到惊讶。 :) - Graham Borland
@GrahamBorland:然而,一旦它被修复以优化一个,它应该同时优化两个。关键是,在(x > -1)(x >= 0)之间不应该有什么值得担心的差异。即使优化只修复了其中一个比较,添加的cmp也几乎是任何程序需要特别注意删除的最后一件事情。 - Michael Burr
显示剩余2条评论

20

你的老师一直在阅读一些很古老的书。以前有些架构缺乏大于等于指令,在这些平台上,计算>所需的机器周期比>=少,但如今这些平台已经很少见了。我建议优先考虑可读性,使用>= 0


但是假设我们有一个非PC架构,比如Arduino。那里会有什么区别吗? - Cheiron
3
编译器有一百万年的历史了,无法发现优化。 - Martin York
3
即使是ATMEL的8位AVR处理器也有BRGE(分支指令,如果大于或等于)和 BRSH(分支指令,如果相同或更高),因此你看不到任何区别。 - Sergey Kalinichenko

14
A bigger concern here is 过早优化。许多人认为编写可读性强的代码比编写高效的代码更加重要[1, 2]。我建议在设计被证明有效之后,将这些优化作为低级库的最后一个阶段来应用。
你不应该不断考虑在代码中进行微小的优化而牺牲可读性,因为这会使阅读和维护代码变得更加困难。如果需要进行这些优化,请将它们抽象成较低级别的函数,以便人类仍然可以更轻松地阅读代码。
作为一个疯狂的例子,考虑一个使用汇编语言编写程序的人和一个愿意放弃额外效率并使用Java来获得其在设计、易用性和可维护性方面的好处的人。
作为一项附注,如果您使用的是C语言,也许编写一个宏来使用稍微更有效的代码是一个更可行的解决方案,因为它将实现效率、可读性和可维护性,而不是分散的操作。
当然,效率和可读性之间的权衡取决于您的应用程序。如果该循环每秒运行10000次,那么它可能是瓶颈,您可能需要投入时间进行优化,但如果它只是偶尔调用的单个语句,则对于微小的收益来说,这可能不值得。

9

是的,有所不同,您应该查看字节码。

对于

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 是因为这样更容易演示,但思路是相同的)


8
你是否开启了优化?JIT 是否不会将其优化掉? - Martin York
2
哇,老师在“哪个更快”这个问题上也错了 :) - Sergey Kalinichenko
for(int x = 10000000; x >= 0; x--) { }<-- 这个测试不会起作用。随机噪声将比差异更长。 - bigGuy
尝试使用Java -Xint Test运行我的测试,它可以正常工作并显示一些差异。 - Evgeniy Dorofeev
请使用硬编码的0和1重复测试,但不要抛出变量n。 - qPCR4vir

4
事实上,我认为第二个版本应该稍微快一些,因为它只需要一个位检查(假设您像上面展示的那样在零处进行比较)。但是这样的优化通常不会显示出来,因为大多数编译器都会优化此类调用。

3
"

">="是一种单独的运算,就像">"一样。而不是使用OR分开两个操作。

但是">=0"可能更快,因为计算机只需要检查一位(负号)。

"

1
我们还需要看一下x是如何获得它的值的(数据流分析)。编译器可能已经知道结果而无需检查任何内容。 - Bo Persson
如果你的编译器比较愚蠢,无法将 x > -1 优化成机器可以高效执行的代码,那么在一些指令集架构(如 MIPS)上,使用 >= 0 可能会更快(例如 MIPS 中有一个 bgez $reg, target 指令,可以根据寄存器的符号位进行分支)。虽然这种方式更快,但并不会使比较本身在软件方面变得更快。所有简单的指令都具有 1 个周期的延迟,无论是 or(独立位)还是 add - Peter Cordes

1

很抱歉打扰这场有关性能的对话。

在我偏离主题之前,让我们注意到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

这些替代方案需要相同的时间,因为指令是相同或类似的,并且它们消耗可预测数量的时钟周期。例如,参见thisjajae可能确实检查不同数量的算术寄存器,但该检查被指令需要花费可预测时间所支配。反过来,这是为了保持CPU架构的可管理性。
但我来到这里是为了离题一下。
在我之前的答案往往是相关的,并且表明无论选择哪种方法,您都将在性能方面处于同一水平。
这就让你基于其他标准做出选择。这就是我想要注意的地方。在测试索引时,优先选择紧绑定样式检查,主要是x >= lowerBound,而不是x > lowerBound - 1。这个参数肯定是牵强的,但归根结底是可读性,因为在这里所有其他因素都是相等的。

从概念上讲,您正在针对一个下限进行测试,x >= lowerBound是最能引起读者适应性认知的规范测试。 x + 10 > lowerBound + 9x - lowerBound >= 0x > -1都是间接测试下限的方法。

再次抱歉打扰,但我觉得这超出了学术范畴的重要性。我总是用这些术语思考,并让编译器担心通过调整常量和运算符的严格性来获得微小的优化。


jajae无符号的大于/大于等于。所有数字都是无符号的>=0,且所有数字都不是>-1U。你需要使用jgjge。此外,请注意,像大多数ISA一样,x86允许与立即数进行比较:cmp ax,0。或者作为优化,test ax,ax将FLAGS设置为与零进行比较的方式相同,但更短。使用CMP reg,0与OR reg,reg测试寄存器是否为零? - Peter Cordes

1
根据这位老师的说法,>比>=略快。在这种情况下,是Java,但他说这也适用于C、C++和其他语言。这种说法是否有道理?
你的老师基本上是错误的。不仅比较与0可能会稍微快一些,而且这种局部优化由编译器/解释器完成得很好,你试图帮助反而会弄巧成拙。绝对不是一个好教导。你可以阅读:thisthis

0

首先,这高度取决于硬件平台。 对于现代个人电脑和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汇编指令而不需要非常昂贵的库调用,肯定会获胜。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接