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

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

本文关键字:gt 性能 是否 vs      更新时间:2023-10-16

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

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

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

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

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

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

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

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

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

  • 我假设一个签名的 int 操作(这似乎是 OP 的意图)
  • 我通过调查将调查限制在 C 和我手头的编译器(诚然,这是一个相当小的样本 - GCC、MSVC 和 IAR)
  • 启用基本优化(-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");
        }
    }
    

以下是他们每个人为比较操作生成的内容:

MSVC 11 针对 ARM:

// 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)之间的任何注释的区别:

  • 面向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# 的抖动输出有任何不同之处,我会感到惊讶。 我怀疑即使对于像 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 可能更快,因为计算机只需要检查一位(负号)。

根据这位老师的说法,>会比>=稍微快一些。在此 如果是Java,但据他说,这也适用于C,C ++ 和其他语言。这种说法有道理吗?

你的老师从根本上是错误的。不仅为什么机会比与 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

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

但我来这里确实是为了离题。

摆在我面前的答案往往是中肯的,也表明无论你选择哪种方法,就性能而言,你都将处于同一个球场。

这让您可以根据其他标准进行选择。这就是我想做笔记的地方。测试索引时,首选紧密绑定样式检查,主要是x >= lowerBound,而不是x > lowerBound - 1。这个论点注定是人为的,但它归结为可读性,因为在这里其他一切都是平等的。

由于从概念上讲,您是在针对下限进行测试,因此x >= lowerBound规范测试可以从代码的读者那里获得最合适的认知。 x + 10 > lowerBound + 9x - lowerBound >= 0x > -1 都是针对下限进行测试的迂回方法。

再次,很抱歉闯入,但我觉得这在学术之外很重要。我总是用这些术语来思考,让编译器担心它认为它可以摆脱对常量和运算符的严格性的摆弄的微小优化。

首先,

它在很大程度上取决于硬件平台。对于现代 PC 和 ARM SoC 来说,差异主要取决于编译器优化。但对于没有 FPU 的 CPU,有符号数学将是灾难。

例如,

简单的 8 位 CPU,例如英特尔 8008、8048、8051、Zilog Z80、摩托罗拉 6800 甚至现代 RISC PIC 或 Atmel 微型控制器通过具有 8 位寄存器的 ALU 进行所有数学运算,基本上只携带标志位和 z(零值指示器)标志位。所有严肃的数学都是通过库和表达式完成的

  BYTE x;
  if (x >= 0) 

肯定会赢,使用 JZ 或 JNZ asm 指令而无需非常昂贵的库调用。

这取决于底层架构。带有Jazzelle的旧ARMv6能够直接执行Java字节码。否则,字节码将转换为机器码。有时,目标平台需要消耗额外的机器周期来创建操作数-10,但另一个平台可能会在比较指令解码时加载它们。其他的,如OpenRISC定义了一个始终保持0的寄存器,可以对其进行比较。最有可能极少数情况下,某些平台需要从较慢的内存加载操作数。总之,运算符的速度不是由Java编程语言指定的,并且概括特定情况会破坏使用跨平台编程语言的目的。