【问题标题】:x > -1 vs x >= 0, is there a performance differencex > -1 vs x >= 0,是否存在性能差异
【发布时间】:2013-01-09 08:37:21
【问题描述】:

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

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

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

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

【问题讨论】:

  • x的类型是...?
  • ... '整数 x' ?
  • @Cheiron:想想如果xuint 类型意味着什么...
  • 表达式对无符号类型毫无意义:第一个永远不会为真,第二个永远为真。
  • Is < faster than <=? 的可能重复项

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


【解决方案1】:

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

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

  • 我假设一个有符号的 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");
        }
    }
    

下面是他们每个人为比较操作产生的结果:

针对 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 &gt; -1)(x &gt;= 0) 之间出现的任何注释的差异:

  • 针对 ARM 的 MSVC 使用 cmp r0,#0xFFFFFFFF 代表 (x &gt; -1)cmp r0,#0 代表 (x &gt;= 0)。第一条指令的操作码长了两个字节。我想这可能会引入一些额外的时间,所以我们将其称为 (x &gt;= 0) 的优势
  • 针对 x86 的 MSVC 使用 cmp ecx, -1 代表 (x &gt; -1)test ecx, ecx 代表 (x &gt;= 0)。第一条指令的操作码长一个字节。我想这可能会带来一些额外的时间,所以我们称之为(x &gt;= 0) 的优势

请注意,GCC 和 IAR 为两种比较生成了相同的机器代码(可能使用了哪个寄存器除外)。因此,根据这项调查,(x &gt;= 0) 似乎“更快”的可能性很小。但是,无论最小化的操作码字节编码可能具有什么优势(我强调可能有),肯定会完全被其他因素所掩盖。

如果您发现 Java 或 C# 的 jitted 输出有什么不同,我会感到惊讶。即使对于像 8 位 AVR 这样非常小的目标,我怀疑你会发现任何不同之处。

简而言之,不用担心这种微优化。我认为我在这里写的时间已经超过了这些表达式在我有生之年执行它们的所有 CPU 上累积的性能差异所花费的时间。如果您有能力衡量性能差异,请将您的精力投入到更重要的事情上,例如研究亚原子粒子的行为或其他事情。

【讨论】:

  • 如果在比较之前需要计算 x 怎么办?....例如,非常常见的 --x ?
  • 我认为这不会对编译器为&gt; -1&gt;= 0 操作生成等效代码的能力产生任何重大影响。
  • 这些代码 sn-ps 并不能真正说明 0-comparison 是免费提供的(至少在 ARM 上),如果 x 刚刚在之前计算过,而 @ 987654349@ 比较需要明确的额外指令。
  • @GrahamBorland:请注意,这里的大多数 ARM 示例处理 x &gt; -1x &gt;= 0 完全相同(即,他们注意到表达式是等价的)。如果计算了x,我希望他们也会这样做——目前我没有一个系统来测试这个假设。另一方面,MSVC ARM 编译器对它们的处理略有不同,我能够测试 MS ARM 编译器。如果计算了 x,它仍然对 -1 和 0 测试进行显式比较(计算后仍然有 cmp r3,#0cmp r3,#0xffffffff)。
  • @MichaelBurr 实际上,MS 编译器未能发现这种明显的优化并不令我感到惊讶。 :)
【解决方案2】:

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

如果有的话,我希望 (x &gt;= 0) 会稍微快一些,因为与 0 相比,在某些指令集(例如 ARM)上是免费的。

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

【讨论】:

  • +1。涉及 0 的事实很可能与两个比较操作本身之间的差异(如果有的话)一样重要(或更多)。
  • @Thilo 这在某些架构上可能是正确的(在这种情况下,我希望编译器自己进行更改)。在其他公司(例如英特尔)上,两者在时间上完全相同。
  • 编辑提到编译器无论如何都会选择最好的。
  • 同意;程序员不应该担心这种级别的细节,除非他们正在对架构进行编程。
  • 我想补充一下 >= 0 比 > -1 更快的原因。这是由于程序集总是与 0 比较。如果第二个值不为 0,则第一个值将与第二个值相加(或减去),之后可能的比较将是 e, lt, le、gt、ge、ne(等于、小于、小于或等于、大于、大于或等于、不等于)。当然,添加的加法/减法需要额外的 CPU 周期。
【解决方案3】:

你的老师一直在读一些非常古老的书。过去,有些架构缺少greater than or equal 指令,评估&gt; 所需的机器周期少于&gt;=,但这些平台现在很少见。我建议提高可读性,并使用&gt;= 0

【讨论】:

  • 但是假设我们有一个非 PC 架构,例如 Arduino。那里会有所作为吗?
  • @Cheiron:而且编译器已有一百万年的历史,无法发现优化。
  • @Cheiron 甚至 ATMEL 的 8 位 AVR 也有 BRGE(如果大于或等于分支)和 BRSH(如果相同或更高则分支)指令,所以你看不出有什么区别。
【解决方案4】:

这里更大的问题是premature optimisation。许多人认为编写可读代码比编写高效代码更重要 [1, 2]。一旦设计被证明可以工作,我会将这些优化作为最后阶段应用到低级库中。

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

作为一个疯狂的例子,考虑一个以汇编形式编写程序的人,而另一个人愿意放弃这种额外的效率并使用 Java 来获得设计、易用性和可维护性方面的好处。

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

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

【讨论】:

    【解决方案5】:

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

    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,因为它更容易演示,想法保持不变)

    【讨论】:

    • 你开启优化了吗? JIT 不会优化它吗?
    • 哇,老师在“哪个更快”上也错了:)
    • for(int x = 10000000; x >= 0; x--) { }
    • 用 java -Xint Test 试试我的测试,它可以工作并且显示出一些不同
    • 请重复测试硬编码0和1,但不要抛出变量n。
    【解决方案6】:

    事实上,我相信第二个版本应该会稍微快一些,因为它需要一个位检查(假设您比较为零,如上所示)。然而,此类优化从未真正显示出来,因为大多数编译器都会优化此类调用。

    【讨论】:

      【解决方案7】:

      ">=" 是单个操作,就像">"。不是使用 OR 进行 2 次单独的操作。

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

      【讨论】:

      • 我们还必须看看x 如何获得它的价值(数据流分析)。编译器可能已经知道结果而不检查任何内容。
      • 如果你的编译器很笨,无法将 x &gt; -1 优化为机器可以有效执行的操作,是的,&gt;= 0 在某些 ISA 上会更快(例如 MIPS,其中有一个 bgez $reg, target 指令,即你说寄存器的符号位上的分支)。更快允许clever hardware design for MIPS internals,但不会使软件的比较本身更快。所有简单指令都有 1 个周期延迟,无论是 or(独立位)还是 add
      【解决方案8】:

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

      你的老师根本就错了。 不仅为什么机会比与 0 进行比较可能更快,而且因为这种本地优化由您的编译器/解释器很好地完成,并且您可以将所有试图提供帮助的人都搞砸。绝对不是什么好教的东西。

      您可以阅读: thisthis

      【讨论】:

        【解决方案9】:

        很抱歉打断这个关于性能的谈话。

        在我离题之前,让我们注意 JVM 有特殊的instructions 不仅可以处理零,还可以处理 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
        

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

        但我确实是来这里跑题的。

        我面前的答案往往是中肯的,并且也表明无论您选择哪种方法,就性能而言,您都将处于同一水平。

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

        由于从概念上讲,您是针对下限进行测试,x &gt;= lowerBound 是标准测试,可以引发代码读者最适应的认知。 x + 10 &gt; lowerBound + 9x - lowerBound &gt;= 0x &gt; -1 都是针对下限进行测试的迂回方式。

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

        【讨论】:

        • jajae 无符号高于/高于或等于。所有数字都是无符号的 >= 0,并且所有数字都不是&gt; -1U。你想要jgjge。另请注意,与大多数 ISA 一样,x86 允许与立即数比较:cmp ax, 0。或者作为一种优化,test ax, ax 将 FLAGS 设置为与零比较相同,但更短。 Test whether a register is zero with CMP reg,0 vs OR reg,reg?
        【解决方案10】:

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

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

          BYTE x;
          if (x >= 0) 
        

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

        【讨论】:

          【解决方案11】:

          这取决于底层架构。带有 Jazzelle 的旧 ARMv6 能够直接执行 Java 字节码。否则,字节码被翻译成机器码。有时,目标平台需要消耗额外的机器周期来创建操作数-10,但另一个可能会在比较指令被解码时加载它们。其他的,例如 OpenRISC 定义了一个持续保持 0 的寄存器,可以对其进行比较。 很可能在极少数情况下,某些平台需要从较慢的内存中加载操作数。 综上所述,Java编程语言并没有规定算子的速度,笼统地概括一个特定的案例就违背了使用跨平台编程语言的目的。

          【讨论】:

          • 所有非玩具架构都有一种方法可以在寄存器中构造小数字,只使用一条不从内存加载的指令,通常类似于mov reg, 0,数字作为立即数。通常这是符号扩展的,因此它也适用于-1。甚至在带有标志的机器上将其用作cmp 指令的立即操作数。像 ARM cmp r1, #-1 / bgt target。此外,即使在没有 mov-immediate 的玩具机器上,您也可以从自身中减去一个寄存器来将其归零。
          • 此外,任何体面的编译器都知道这些技巧,如果这样更有效,它们会将x &gt; -1 转换为x&gt;=0。您的回答假定 Java 源表达式将直接转译为机器代码,而无需尝试在目标机器上找到更有效的方法来执行相同的操作。但无论如何,所有现实世界的机器都可以有效地将一个值与0 进行比较。
          • 嗯是的,这是真的,但我的意思是这取决于底层架构。如果平台不直接执行 java 字节码,它可能会被翻译成机器码。此外,与寄存器本身相减也被认为与零比较比寄存器直接保持零或机器可以直接与零比较慢。同样,这完全取决于平台,语言并不能保证哪个运算符更快或更慢
          • 理论上它可能依赖于 ISA,但前提是编译器是愚蠢的并且不知道这种窥视孔优化。 (对于 JIT 来说是合理的,但我想看一个例子)。即便如此,这并不是因为您在答案中陈述的原因:从数据存储器加载0-1 对于任何人都关心的现实世界的ISA 来说是不合理的。 (仅适用于 MARIE 或 LCM 之类的玩具,无论如何都不能用作编译器目标。)如果您想谈论直接执行 Java 字节码的硬件,请将 that 作为合理的真实情况放在您的答案中-世界特例。
          • 如果你想做一个体面的案例,你可以指出 MIPS 有特殊的指令来比较和分支与零,比如bgez,但实际上是在不执行 x &gt; -1 的情况下实现简单的优化需要slti $t0, $a0, -1 / bne $t0, $zero, target。或者 RISC-V 是类似的,你需要一个 -1 在一个寄存器中,但零寄存器已经在那里了。但是,大多数带有 FLAGS / 某种状态寄存器(ARM、PowerPC、x86)的机器需要在分支之前进行比较,并且与立即比较 0-1 在 RISC 上的成本相同,因此零 reg 没有帮助.
          猜你喜欢
          • 2015-08-09
          • 2021-01-24
          • 1970-01-01
          • 2017-03-02
          • 1970-01-01
          • 1970-01-01
          • 2021-11-28
          • 2012-08-21
          • 1970-01-01
          相关资源
          最近更新 更多