关于java:x > -1和x >= 0, 有什么差别吗

x > -1 vs x >= 0, is there a performance difference

我听过一位老师把这个丢了一次,从那以后它就一直在烦我。假设我们要检查整数x是否大于或等于0。有两种检查方法:

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

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

据这位老师说,>>=快一点。在这种情况下,它是Java,但根据他,这也适用于C、C++和其他语言。这个说法是真的吗?


在现实世界中没有任何区别。

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

  • 我假设一个有符号的int操作(这似乎是操作的意图)
  • 我的调查局限于C和我手头上的编译器(不可否认是一个很小的样本——GCC、MSVC和IAR)。
  • 已启用基本优化(gcc为-O2,msvc为/Ox,iar为-Oh)
  • 使用以下模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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");
        }
    }

下面是它们各自为比较操作产生的结果:

MSVC 11目标臂:

1
2
3
4
5
6
7
8
9
10
// 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为目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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:

1
2
3
4
5
6
7
8
9
10
// 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

1
2
3
4
5
6
7
8
9
10
11
// 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:

1
2
3
4
5
6
7
8
9
10
11
// 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

一般合同条款第4.4.1款:目标武器:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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针对手臂皮质-M3:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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)之间的区别,显示:

  • MSVC靶向臂使用cmp r0,#0xFFFFFFFF作为(x > -1)vs 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语言的JET输出有什么不同,我会感到惊讶。我怀疑你会发现任何不同的注意,即使是一个非常小的目标,如8位的AVR。

简而言之,不要担心这种微观优化。我想我在这里写的时间已经超过了在我有生之年执行这些表达式的所有CPU中积累的这些表达式的性能差异所花费的时间。如果您有能力测量性能差异,请将您的努力应用于更重要的事情,如研究亚原子粒子或其他东西的行为。


它在很大程度上依赖于底层架构,但是任何差异都是微不足道的。

如果有的话,我希望(x >= 0)稍微快一点,因为与0相比,有些指令集(如arm)是免费的。

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


你的老师一直在读一些非常古老的书。过去有些体系结构缺少greater than or equal指令,评估>所需的机器周期比>=少,但现在这些平台很少。我建议去读,并使用>= 0


更大的问题是过早的优化。许多人认为写可读代码比写有效代码更重要[1,2]。一旦设计被证明有效,我将把这些优化作为低级库的最后一个阶段来应用。

你不应该一直在考虑以牺牲可读性为代价对代码进行微小的优化,因为这会使代码的阅读和维护更加困难。如果需要进行这些优化,请将它们抽象为较低级别的函数,这样您仍然可以获得更容易为人类阅读的代码。

作为一个疯狂的例子,考虑一个在汇编中编写程序的人,他们愿意放弃额外的效率,并在设计、使用方便和可维护性方面使用Java。

作为补充说明,如果您使用的是C,那么编写一个使用稍微高效的代码的宏可能是一个更可行的解决方案,因为它比分散的操作更能实现效率、可读性和可维护性。

当然,效率和可读性的权衡取决于您的应用程序。如果这个循环每秒运行10000次,那么它可能是一个瓶颈,您可能想花时间优化它,但是如果只是一个单独的语句,偶尔被调用,那么它可能不值得一分钟的时间来获得它。


是的,有区别,你应该看到字节码。

对于

1
2
    if (x >= 0) {
    }

字节码是

1
2
    ILOAD 1
    IFLT L1

对于

1
2
if (x > -1) {
}

字节码是

1
2
3
ILOAD 1
ICONST_M1
IF_ICMPLE L3

版本1更快,因为它使用特殊的零操作数操作

1
iflt : jump if less than zero

但是,可以看到仅在解释模式java -Xint ...下运行JVM的区别,例如这个测试

1
2
3
4
5
6
7
8
9
    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,因为它更容易演示,所以想法保持不变)


事实上,我认为第二个版本应该稍微快一点,因为它需要一个位检查(假设您在0处进行比较,如上面所示)。然而,这种优化从未真正显示,因为大多数编译器将优化此类调用。


">="是单一操作,就像">"。不是两个单独的操作,带有或。

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


很抱歉打断了关于表演的谈话。

在我离题之前,让我们注意到,JVM不仅有处理零的特殊指令,而且还有从1到3的常量。有了这一点,体系结构处理零的能力很可能早就失去了,不仅仅是编译器优化,还有字节码到机器代码的转换等等。

我记得在我的x86汇编语言时代,在集合中有大于(ja和大于或等于(jae的指令。你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
; 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

这些替代方法所用的时间相同,因为指令相同或相似,并且它们消耗可预测的时钟周期数。例如,请参见。jajae的确可以检查不同数量的算术寄存器,但这种检查主要是需要指令花费可预测的时间。这反过来又需要保持CPU体系结构的可管理性。

但我来这里是为了离题。

在我面前的答案往往是有针对性的,同时也表明无论你选择哪种方法,你在表现方面都会处于同样的状态。

这就让您可以根据其他标准进行选择。这就是我要做记录的地方。在测试指标时,与x > lowerBound - 1相比,更倾向于紧约束样式检查,主要是x >= lowerBound。这个论点一定是人为的,但归根结底就是可读性,因为这里所有其他的都是真正平等的。

因为从概念上讲,您是在测试一个下限,所以x >= lowerBound是从代码的读者那里获得最适合的认知的规范测试。x + 10 > lowerBound + 9x - lowerBound >= 0x > -1都是针对下限的全面测试方法。

再次,很抱歉打扰你,但我觉得这比学术界的事情更重要。我总是用这些术语来思考,让编译器担心它认为可以摆脱对常量和运算符严格性的修改的微小优化。


According to this teacher > would be slightly faster then >=. In this
case it was Java, but according to him this also applied for C, c++
and other languages. Is there any truth to this statement?

你的老师根本是错的。不仅"机会"比"0"的速度要快得多,而且因为这种局部优化是由编译器/解释器很好地完成的,而且你会把所有试图帮助的事情都搞砸。毫无疑问,教书不是件好事。

你可以阅读:这个或这个


首先,它高度依赖硬件平台。对于现代PC和ARM SOC,差异主要依赖于编译器优化。但是对于没有FPU的CPU来说,签名数学将是灾难。

例如,简单的8位CPU,如Intel 8008、80488051、Zilog Z80、Motorola 6800,甚至现代RISC PIC或Atmel微处理器都通过8位寄存器的ALU进行所有数学运算,基本上只有进位标志位和Z(零值指示器)标志位。所有严肃的数学都是通过库和表达式完成的。

1
2
  BYTE x;
  if (x >= 0)

如果使用JZ或JNZ ASM指令而不调用非常昂贵的库,肯定会赢。