x> -1 vs x> = 0,是否存在性能差异

时间:2013-01-25 11:25:12

标签: java c++ operators micro-optimization premature-optimization

我听过一位老师放弃了这一次,从那以后一直困扰着我。假设我们要检查整数x是否大于或等于0.有两种方法可以检查:

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

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

根据这位老师>,会比>=略快一些。在这种情况下它是Java,但据他说,这也适用于C,c ++和其他语言。这句话有什么道理吗?

11 个答案:

答案 0 :(得分:29)

它非常依赖于底层架构,但任何差异都是微不足道的。

如果有的话,我希望(x >= 0)稍快一些,因为与0的比较在某些指令集(例如ARM)上免费提供。

当然,任何合理的编译器都会选择最佳实现,无论您的源代码是哪种变体。

答案 1 :(得分:29)

在任何现实世界的意义上都没有区别。

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

  • 我假设一个有符号的int操作(这似乎是OP的意图)
  • 我对C及其编制人员的调查受到限制(我认为这是一个非常小的样本 - GCC,MSVC和IAR)
  • 已启用基本优化(GCC为-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)之间的任何注释都会有所不同:

  • 针对ARM的MSVC对cmp r0,#0xFFFFFFFF使用(x > -1)cmp r0,#0使用(x >= 0)。第一条指令的操作码长两个字节。我想这可能会引入一些额外的时间,所以我们称之为(x >= 0)
  • 的优势
  • MSVC定位x86对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。许多人认为编写可读代码比编写高效代码[12]更重要。一旦设计被证明有效,我会将这些优化应用为低级库中的最后一个阶段。

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

作为一个疯狂的例子,考虑一下将他们的程序编写成一个愿意放弃额外效率并使用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比较可以很快,但因为这种局部优化很好地由你的编译器/解释器完成,你可以把所有尝试帮助的东西弄得一团糟。绝对不是一件好事。

您可以阅读: thisthis

答案 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

这些替代方案花费相同的时间,因为指令是相同或相似的,并且它们消耗可预测的时钟周期数。例如,请参阅thisjajae可能确实检查了不同数量的算术寄存器,但该检查主要是指令需要采取可预测的时间。这反过来又需要保持CPU架构的可管理性。

但我来这里是为了离题。

在我之前的答案往往是恰当的,并且表明无论你选择哪种方法,你都会在表演方面处于相同的范围内。

这使您可以根据其他标准进行选择。这就是我想做笔记的地方。在测试索引时,更喜欢紧密绑定样式检查(主要是x >= lowerBound)到x > lowerBound - 1。这个论点必然是人为的,但它归结为可读性,因为这里所有其他的都是平等的。

从概念上讲,你是在测试下限,x >= lowerBound是规范测试,可以从你的代码读者那里获得最适应的认知。 x + 10 > lowerBound + 9x - lowerBound >= 0x > -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 字节码。否则,字节码被翻译成机器码。有时,目标平台需要消耗额外的机器周期来创建操作数 -10,但另一个可能会在解码比较指令时加载它们。其他的,例如 OpenRISC 定义了一个寄存器,它一直保持为 0,可以进行比较。 很可能 某些平台很少需要从较慢的内存中加载操作数。 综上所述,操作符的速度不是Java编程语言所规定的,泛化一个特定的案例就违背了使用跨平台编程语言的目的。