在Java中,可以&要快于&&&?

时间:2016-09-20 07:34:19

标签: java performance processing-efficiency microbenchmark branch-prediction

在此代码中:

if (value >= x && value <= y) {

value >= xvalue <= y可能为false而没有特定模式时,使用&运算符比使用&&

具体来说,我正在思考&&如何懒惰地评估右侧表达式(即仅当LHS为真时),这意味着有条件,而在这种情况下,Java &保证严格评估两个(布尔)子表达式。值结果都是相同的。

但是>=<=运算符将使用简单的比较指令,&&必须包含分支,该分支容易受到分支预测失败 - 根据这个非常着名的问题:Why is it faster to process a sorted array than an unsorted array?

因此,强制表达式没有延迟组件肯定会更具确定性,并且不容易受到预测失败的影响。正确?

注意:

  • 显然,如果代码如下所示,我的问题的答案将是if(value >= x && verySlowFunction())。我专注于“足够简单”的RHS表达。
  • 无论如何都有一个条件分支(if语句)。我不能完全向自己证明这是无关紧要的,而替代的表述可能是更好的例子,例如boolean b = value >= x && value <= y;
  • 这一切都落入了可怕的微观优化世界。是的,我知道:-) ...虽然很有趣?

更新 只是为了解释为什么我感兴趣:我一直在盯着马丁汤普森关于他Mechanical Sympathy blog的文章,他来到did a talk之后关于Aeron。其中一个关键信息是我们的硬件中包含了所有这些神奇的东西,我们的软件开发人员不幸地无法利用它。别担心,我不打算在我的所有代码上使用/&amp; / \ /&amp; /:... ...但是这个网站上有很多关于通过删除分支来改进分支预测的问题在我看来,条件布尔运算符是在测试条件的核心

当然,@ StephenC提出了一个奇妙的观点,即将代码弯曲成奇怪的形状可以使JIT更容易发现常见的优化 - 如果不是现在,那么将来。并且上面提到的非常着名的问题是特殊的,因为它推动预测复杂性远远超出实际优化。

我非常清楚,在大多数(或几乎所有)情况下,&&是最清晰,最简单,最快速,最好的事情 - 尽管我非常感激对已发布答案证明此事的人们!我真的很想知道在任何人的经历中是否存在“Can &能否更快”的答案?可能 ...

更新2 (解决这个问题过于宽泛的建议。我不想对这个问题做出重大改变,因为它可能会影响下面的一些质量非常好的答案!)也许是一个例子。要求野外;这来自Guava LongMath课程(非常感谢@maaartinus发现这个):

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

首先看到&?如果你检查链接, next 方法被称为lessThanBranchFree(...),它暗示我们处于分支避免领域 - 并且番石榴确实被广泛使用:每个保存的周期都会导致海平面明显地掉下来。所以让我们用这样的方式提出问题:使用&(其中&&会更正常)真正的优化吗?

7 个答案:

答案 0 :(得分:71)

好的,所以你想知道它在较低级别的表现......让我们来看看字节码吧!

编辑:最后为AMD64添加了生成的汇编代码。看一些有趣的笔记。
EDIT 2(re:OP&#39; s&#34; Update 2&#34;):同时添加了Guava's isPowerOfTwo method的asm代码。

Java源

我写了这两个快速方法:

public boolean AndSC(int x, int value, int y) {
    return value >= x && value <= y;
}

public boolean AndNonSC(int x, int value, int y) {
    return value >= x & value <= y;
}

如您所见,它们完全相同,除了AND运算符的类型。

Java字节码

这是生成的字节码:

  public AndSC(III)Z
   L0
    LINENUMBER 8 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1
   L2
    LINENUMBER 9 L2
    ICONST_1
    IRETURN
   L1
    LINENUMBER 11 L1
   FRAME SAME
    ICONST_0
    IRETURN
   L3
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L3 0
    LOCALVARIABLE x I L0 L3 1
    LOCALVARIABLE value I L0 L3 2
    LOCALVARIABLE y I L0 L3 3
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x1
  public AndNonSC(III)Z
   L0
    LINENUMBER 15 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ICONST_1
    GOTO L2
   L1
   FRAME SAME
    ICONST_0
   L2
   FRAME SAME1 I
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L3
    ICONST_1
    GOTO L4
   L3
   FRAME SAME1 I
    ICONST_0
   L4
   FRAME FULL [test/lsoto/AndTest I I I] [I I]
    IAND
    IFEQ L5
   L6
    LINENUMBER 16 L6
    ICONST_1
    IRETURN
   L5
    LINENUMBER 18 L5
   FRAME SAME
    ICONST_0
    IRETURN
   L7
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L7 0
    LOCALVARIABLE x I L0 L7 1
    LOCALVARIABLE value I L0 L7 2
    LOCALVARIABLE y I L0 L7 3
    MAXSTACK = 3
    MAXLOCALS = 4

AndSC&&)方法按预期生成两个条件跳转:

  1. valuex加载到堆栈中,如果value较低,则跳转到L1。否则它会继续运行下一行。
  2. 它会将valuey加载到堆栈上,如果value更大,它也会跳转到L1。否则它会继续运行下一行。
  3. 如果没有两次跳跃,则碰巧是return true
  4. 然后我们将标记为L1的行标记为return false
  5. 然而,AndNonSC&)方法会生成三个条件跳转!

    1. 如果value较低,它会将xvalue加载到堆栈上并跳转到L1。因为现在它需要保存结果以将其与AND的其他部分进行比较,因此它必须执行&#34;保存true&#34;或者&#34;保存false&#34;,它不能同时使用相同的指令。
    2. 如果value更大,它会将yvalue加载到堆栈上并跳转到L1。根据比较结果,它需要再次保存truefalse以及两条不同的行。
    3. 现在两个比较完成后,代码实际执行AND操作 - 如果两者都为真,则跳转(第三次)返回true;否则它会继续执行到下一行以返回false。
    4. (初步)结论

      虽然我对Java字节码没有那么多经验而且我可能忽略了某些东西,但在我看来,&实际上会比{{1}更多地执行更糟在每种情况下:它生成更多的指令来执行,包括更多的条件跳转来预测并可能失败。

      如同其他人的建议,重写代码以替代与算术运算的比较可能是使&&成为更好选择的一种方法,但代价是使代码更不清晰。
      恕我直言,99%的情况都不值得麻烦(尽管如此,对于需要进行极度优化的1%循环来说,这可能非常值得。)

      编辑:AMD64程序集

      如注释中所述,相同的Java字节码可能导致不同系统中的不同机器代码,因此虽然Java字节码可能会提示我们哪个AND版本的性能更好,但是获取编译器生成的实际ASM是真正找到答案的唯一方法 我为这两种方法打印了AMD64 ASM指令;以下是相关的行(剥离的入口点等)。

      注意:除非另有说明,否则使用java 1.8.0_91编译的所有方法。

      方法&,默认选项

      AndSC

      # {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest' ... 0x0000000002923e3e: cmp %r8d,%r9d 0x0000000002923e41: movabs $0x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')} 0x0000000002923e4b: movabs $0x108,%rsi 0x0000000002923e55: jl 0x0000000002923e65 0x0000000002923e5b: movabs $0x118,%rsi 0x0000000002923e65: mov (%rax,%rsi,1),%rbx 0x0000000002923e69: lea 0x1(%rbx),%rbx 0x0000000002923e6d: mov %rbx,(%rax,%rsi,1) 0x0000000002923e71: jl 0x0000000002923eb0 ;*if_icmplt ; - AndTest::AndSC@2 (line 22) 0x0000000002923e77: cmp %edi,%r9d 0x0000000002923e7a: movabs $0x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')} 0x0000000002923e84: movabs $0x128,%rsi 0x0000000002923e8e: jg 0x0000000002923e9e 0x0000000002923e94: movabs $0x138,%rsi 0x0000000002923e9e: mov (%rax,%rsi,1),%rdi 0x0000000002923ea2: lea 0x1(%rdi),%rdi 0x0000000002923ea6: mov %rdi,(%rax,%rsi,1) 0x0000000002923eaa: jle 0x0000000002923ec1 ;*if_icmpgt ; - AndTest::AndSC@7 (line 22) 0x0000000002923eb0: mov $0x0,%eax 0x0000000002923eb5: add $0x30,%rsp 0x0000000002923eb9: pop %rbp 0x0000000002923eba: test %eax,-0x1c73dc0(%rip) # 0x0000000000cb0100 ; {poll_return} 0x0000000002923ec0: retq ;*ireturn ; - AndTest::AndSC@13 (line 25) 0x0000000002923ec1: mov $0x1,%eax 0x0000000002923ec6: add $0x30,%rsp 0x0000000002923eca: pop %rbp 0x0000000002923ecb: test %eax,-0x1c73dd1(%rip) # 0x0000000000cb0100 ; {poll_return} 0x0000000002923ed1: retq 选项的方法AndSC

      -XX:PrintAssemblyOptions=intel

      方法 # {method} {0x00000000170a0810} 'AndSC' '(III)Z' in 'AndTest' ... 0x0000000002c26e2c: cmp r9d,r8d 0x0000000002c26e2f: jl 0x0000000002c26e36 ;*if_icmplt 0x0000000002c26e31: cmp r9d,edi 0x0000000002c26e34: jle 0x0000000002c26e44 ;*iconst_0 0x0000000002c26e36: xor eax,eax ;*synchronization entry 0x0000000002c26e38: add rsp,0x10 0x0000000002c26e3c: pop rbp 0x0000000002c26e3d: test DWORD PTR [rip+0xffffffffffce91bd],eax # 0x0000000002910000 0x0000000002c26e43: ret 0x0000000002c26e44: mov eax,0x1 0x0000000002c26e49: jmp 0x0000000002c26e38 ,默认选项

      AndNonSC

      # {method} {0x0000000016da0908} 'AndNonSC' '(III)Z' in 'AndTest' ... 0x0000000002923a78: cmp %r8d,%r9d 0x0000000002923a7b: mov $0x0,%eax 0x0000000002923a80: jl 0x0000000002923a8b 0x0000000002923a86: mov $0x1,%eax 0x0000000002923a8b: cmp %edi,%r9d 0x0000000002923a8e: mov $0x0,%esi 0x0000000002923a93: jg 0x0000000002923a9e 0x0000000002923a99: mov $0x1,%esi 0x0000000002923a9e: and %rsi,%rax 0x0000000002923aa1: cmp $0x0,%eax 0x0000000002923aa4: je 0x0000000002923abb ;*ifeq ; - AndTest::AndNonSC@21 (line 29) 0x0000000002923aaa: mov $0x1,%eax 0x0000000002923aaf: add $0x30,%rsp 0x0000000002923ab3: pop %rbp 0x0000000002923ab4: test %eax,-0x1c739ba(%rip) # 0x0000000000cb0100 ; {poll_return} 0x0000000002923aba: retq ;*ireturn ; - AndTest::AndNonSC@25 (line 30) 0x0000000002923abb: mov $0x0,%eax 0x0000000002923ac0: add $0x30,%rsp 0x0000000002923ac4: pop %rbp 0x0000000002923ac5: test %eax,-0x1c739cb(%rip) # 0x0000000000cb0100 ; {poll_return} 0x0000000002923acb: retq 选项的方法AndNonSC

      -XX:PrintAssemblyOptions=intel
      • 首先,生成的ASM代码根据我们选择默认的AT&amp; T语法还是Intel语法而有所不同。
      • 使用AT&amp; T语法:
        • # {method} {0x00000000170a0908} 'AndNonSC' '(III)Z' in 'AndTest' ... 0x0000000002c270b5: cmp r9d,r8d 0x0000000002c270b8: jl 0x0000000002c270df ;*if_icmplt 0x0000000002c270ba: mov r8d,0x1 ;*iload_2 0x0000000002c270c0: cmp r9d,edi 0x0000000002c270c3: cmovg r11d,r10d 0x0000000002c270c7: and r8d,r11d 0x0000000002c270ca: test r8d,r8d 0x0000000002c270cd: setne al 0x0000000002c270d0: movzx eax,al 0x0000000002c270d3: add rsp,0x10 0x0000000002c270d7: pop rbp 0x0000000002c270d8: test DWORD PTR [rip+0xffffffffffce8f22],eax # 0x0000000002910000 0x0000000002c270de: ret 0x0000000002c270df: xor r8d,r8d 0x0000000002c270e2: jmp 0x0000000002c270c0 方法的ASM代码实际上更长,每个字节码AndSC都转换为两个汇编跳转指令,总共有4个条件跳转。
        • 同时,对于IF_ICMP*方法,编译器生成更直接的代码,其中每个字节码AndNonSC仅转换为一个汇编跳转指令,保持原始计数为3个条件跳转。
      • 使用Intel语法:
        • IF_ICMP*的ASM代码较短,仅有2个条件跳转(不包括最后的非条件AndSC)。实际上,根据结果,它只有两个CMP,两个JL / E和一个XOR / MOV。
        • jmp的ASM代码现在比AndNonSC长! 然而,它只有1次条件跳转(第一次比较),使用寄存器直接比较第一个结果和第二个结果,而不再跳转。

      ASM代码分析后的结论

      • 在AMD64机器语言级别,AndSC运算符似乎生成具有较少条件跳转的ASM代码,这对于高预测失败率(例如,随机&)可能更好。
      • 另一方面,value运算符似乎生成带有较少指令的ASM代码(无论如何都使用&&选项),这对于非常长的循环可能更好具有预测友好的输入,其中每次比较的CPU周期数较少,从长远来看会有所不同。

      正如我在一些评论中所说,系统之间的差别很大,所以如果我们谈论分支预测优化,唯一真正的答案是:它取决于你的JVM实现,编译器,CPU和输入数据

      附录:番石榴的-XX:PrintAssemblyOptions=intel方法

      在这里,Guava的开发者提出了一种巧妙的方法来计算给定数字是否为2的幂:

      isPowerOfTwo

      引用OP:

        

      使用public static boolean isPowerOfTwo(long x) { return x > 0 & (x & (x - 1)) == 0; } (其中&会更正常)真正的优化吗?

      为了确定它是否是,我在测试类中添加了两个类似的方法:

      &&

      针对番石榴版本的英特尔ASM代码

      public boolean isPowerOfTwoAND(long x) {
          return x > 0 & (x & (x - 1)) == 0;
      }
      
      public boolean isPowerOfTwoANDAND(long x) {
          return x > 0 && (x & (x - 1)) == 0;
      }
      

      英特尔 # {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest' # this: rdx:rdx = 'AndTest' # parm0: r8:r8 = long ... 0x0000000003103bbe: movabs rax,0x0 0x0000000003103bc8: cmp rax,r8 0x0000000003103bcb: movabs rax,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')} 0x0000000003103bd5: movabs rsi,0x108 0x0000000003103bdf: jge 0x0000000003103bef 0x0000000003103be5: movabs rsi,0x118 0x0000000003103bef: mov rdi,QWORD PTR [rax+rsi*1] 0x0000000003103bf3: lea rdi,[rdi+0x1] 0x0000000003103bf7: mov QWORD PTR [rax+rsi*1],rdi 0x0000000003103bfb: jge 0x0000000003103c1b ;*lcmp 0x0000000003103c01: movabs rax,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')} 0x0000000003103c0b: inc DWORD PTR [rax+0x128] 0x0000000003103c11: mov eax,0x1 0x0000000003103c16: jmp 0x0000000003103c20 ;*goto 0x0000000003103c1b: mov eax,0x0 ;*lload_1 0x0000000003103c20: mov rsi,r8 0x0000000003103c23: movabs r10,0x1 0x0000000003103c2d: sub rsi,r10 0x0000000003103c30: and rsi,r8 0x0000000003103c33: movabs rdi,0x0 0x0000000003103c3d: cmp rsi,rdi 0x0000000003103c40: movabs rsi,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')} 0x0000000003103c4a: movabs rdi,0x140 0x0000000003103c54: jne 0x0000000003103c64 0x0000000003103c5a: movabs rdi,0x150 0x0000000003103c64: mov rbx,QWORD PTR [rsi+rdi*1] 0x0000000003103c68: lea rbx,[rbx+0x1] 0x0000000003103c6c: mov QWORD PTR [rsi+rdi*1],rbx 0x0000000003103c70: jne 0x0000000003103c90 ;*lcmp 0x0000000003103c76: movabs rsi,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')} 0x0000000003103c80: inc DWORD PTR [rsi+0x160] 0x0000000003103c86: mov esi,0x1 0x0000000003103c8b: jmp 0x0000000003103c95 ;*goto 0x0000000003103c90: mov esi,0x0 ;*iand 0x0000000003103c95: and rsi,rax 0x0000000003103c98: and esi,0x1 0x0000000003103c9b: mov rax,rsi 0x0000000003103c9e: add rsp,0x50 0x0000000003103ca2: pop rbp 0x0000000003103ca3: test DWORD PTR [rip+0xfffffffffe44c457],eax # 0x0000000001550100 0x0000000003103ca9: ret 版的asm代码

      &&

      在这个具体的例子中,JIT编译器为 # {method} {0x0000000017580bd0} 'isPowerOfTwoANDAND' '(J)Z' in 'AndTest' # this: rdx:rdx = 'AndTest' # parm0: r8:r8 = long ... 0x0000000003103438: movabs rax,0x0 0x0000000003103442: cmp rax,r8 0x0000000003103445: jge 0x0000000003103471 ;*lcmp 0x000000000310344b: mov rax,r8 0x000000000310344e: movabs r10,0x1 0x0000000003103458: sub rax,r10 0x000000000310345b: and rax,r8 0x000000000310345e: movabs rsi,0x0 0x0000000003103468: cmp rax,rsi 0x000000000310346b: je 0x000000000310347b ;*lcmp 0x0000000003103471: mov eax,0x0 0x0000000003103476: jmp 0x0000000003103480 ;*ireturn 0x000000000310347b: mov eax,0x1 ;*goto 0x0000000003103480: and eax,0x1 0x0000000003103483: add rsp,0x40 0x0000000003103487: pop rbp 0x0000000003103488: test DWORD PTR [rip+0xfffffffffe44cc72],eax # 0x0000000001550100 0x000000000310348e: ret 版本生成组装代码而不是Guava的&&版本(并且,在昨天之后&# 39;结果,我真的很惊讶这个。) 与Guava相比,&版本转换为JIT编译的字节码减少25%,汇编指令减少50%,只有两个条件跳转(&&版本有四个)

      所以所有事情都指向番石榴的&方法效率低于更多&#34;天然&#34; &版本。

      ......或者是吗?

      如前所述,我使用Java 8运行上述示例:

      &&

      如果我切换到Java 7 怎么办?

      C:\....>java -version
      java version "1.8.0_91"
      Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
      Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
      

      惊喜!由Java 7中的JIT编译器为C:\....>c:\jdk1.7.0_79\bin\java -version java version "1.7.0_79" Java(TM) SE Runtime Environment (build 1.7.0_79-b15) Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode) C:\....>c:\jdk1.7.0_79\bin\java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*AndTest.isPowerOfTwoAND -XX:PrintAssemblyOptions=intel AndTestMain ..... 0x0000000002512bac: xor r10d,r10d 0x0000000002512baf: mov r11d,0x1 0x0000000002512bb5: test r8,r8 0x0000000002512bb8: jle 0x0000000002512bde ;*ifle 0x0000000002512bba: mov eax,0x1 ;*lload_1 0x0000000002512bbf: mov r9,r8 0x0000000002512bc2: dec r9 0x0000000002512bc5: and r9,r8 0x0000000002512bc8: test r9,r9 0x0000000002512bcb: cmovne r11d,r10d 0x0000000002512bcf: and eax,r11d ;*iand 0x0000000002512bd2: add rsp,0x10 0x0000000002512bd6: pop rbp 0x0000000002512bd7: test DWORD PTR [rip+0xffffffffffc0d423],eax # 0x0000000002120000 0x0000000002512bdd: ret 0x0000000002512bde: xor eax,eax 0x0000000002512be0: jmp 0x0000000002512bbf ..... 方法生成的汇编代码现在只有一个条件跳转,并且更短!而&方法(你不得不相信我这个方法,我不想让结局混乱!)仍然大致相同,有两个条件跳转和少一些指令,上衣。
      毕竟看起来Guava的工程师知道他们在做什么! (如果他们试图优化Java 7执行时间,那就是; - )

      回到OP的最新问题:

        

      使用&&(其中&会更正常)真正的优化吗?

      恕我直言答案是相同的,即使对于这个(非常!)特定情况:它取决于您的JVM实现,编译器,CPU和输入数据

答案 1 :(得分:23)

对于那些问题,你应该运行一个微基准测试。我使用JMH进行了此测试。

基准测试实现为

// boolean logical AND
bh.consume(value >= x & y <= value);

// conditional AND
bh.consume(value >= x && y <= value);

// bitwise OR, as suggested by Joop Eggen
bh.consume(((value - x) | (y - value)) >= 0)

根据基准名称使用value, x and y的值。

吞吐量基准测试的结果(五次预热和十次测量迭代)是:

Benchmark                                 Mode  Cnt    Score    Error   Units
Benchmark.isBooleanANDBelowRange          thrpt   10  386.086 ▒ 17.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.240 ▒  7.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.847 ▒ 15.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.877 ▒ 11.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.743 ▒ 15.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.524 ▒ 16.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.190 ▒ 19.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.094 ▒ 15.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.913 ▒  5.537  ops/us

评估本身的结果并没有那么不同。只要在那段代码上没有发现性能影响,我就不会尝试对其进行优化。根据代码中的位置,热点编译器可能会决定进行一些优化。上述基准可能没有涵盖这一点。

一些参考文献:

boolean logical AND - 如果两个操作数值均为true,则结果值为true;否则,结果为false
conditional AND - 与&类似,但仅当其左侧操作数的值为true时才计算其右侧操作数。 bitwise OR - 结果值是操作数值

的按位包含OR

答案 2 :(得分:12)

我将从另一个角度来看待这个问题。

考虑这两个代码片段,

  if (value >= x && value <= y) {

  if (value >= x & value <= y) {

如果我们假设valuexy具有基本类型,那么这两个(部分)语句将为所有可能的输入值提供相同的结果。 (如果涉及包装器类型,那么它们并不完全等效,因为null的{​​{1}}测试可能会在y版本而不是&版本中失败。 )

如果JIT编译器做得很好,它的优化器将能够推断出这两个语句做同样的事情:

  • 如果一个比另一个更快,那么它应该能够在JIT编译代码中使用更快的版本...

  • 如果没有,那么在源代码级别使用哪个版本并不重要。

  • 由于JIT编译器在编译之前收集路径统计信息,因此它可能有更多关于程序员(!)的执行特性的信息。

  • 如果当前一代JIT编译器(在任何给定的平台上)没有足够好地进行优化来处理这个问题,那么下一代就可以做到......取决于经验证据是否表明这是一个< em>值得模式进行优化。

  • 的确,如果你以优化的方式为你编写Java代码,那么有机会通过选择更“模糊”的代码版本,你可能禁止当前或未来JIT编译器的优化能力。

简而言之,我认为你不应该在源代码级别进行这种微优化。如果你接受这个论点 1 ,并按照它的逻辑结论,那么哪个版本更快的问题是...... moot 2

1 - 我并未声称这可以作为证明。

2 - 除非你是真正编写Java JIT编译器的人群之一......

“非常着名的问题”在两个方面很有意思:

  • 一方面,这是一个例子,其中产生差异所需的优化类型超出了JIT编译器的能力。

  • 另一方面,排序数组不一定是正确的事情......只是因为可以更快地处理排序的数组。排序数组的成本很可能(远远大于)节省。

答案 3 :(得分:6)

使用&&&仍需要评估条件,因此不太可能节省任何处理时间 - 考虑到您只需要评估两个表达式,它甚至可能会添加它评估一个。

使用&超过&&来节省纳秒,如果在极少数情况下毫无意义,那么你已经浪费了更多时间来考虑差异,而不是使用{{1} } &

修改

我很好奇,决定跑一些基准。

我上了这堂课:

&&

并使用NetBeans运行一些分析测试。我没有使用任何打印语句来节省处理时间,只知道它们都评估为public class Main { static int x = 22, y = 48; public static void main(String[] args) { runWithOneAnd(30); runWithTwoAnds(30); } static void runWithOneAnd(int value){ if(value >= x & value <= y){ } } static void runWithTwoAnds(int value){ if(value >= x && value <= y){ } } }

第一次测试:

The first profiling test

第二次测试:

The second profiling test

第三次测试:

The third profiling test

正如您在分析测试中所看到的,与使用两个true相比,仅使用一个&实际上需要花费2-3倍的时间。这确实发生了一些奇怪的事情,因为我只期望从一个&&获得更好的表现。

我不是百分百肯定为什么。在这两种情况下,两个表达式都必须进行评估,因为两者都是真的。我怀疑JVM在幕后做了一些特殊的优化来加速它。

故事的道德:惯例是好的,过早的优化是坏的。

修改2

我用@ SvetlinZarev的评论和其他一些改进来重新编写基准代码。以下是修改后的基准代码:

&

以下是性能测试:

测试1:

enter image description here

测试2:

enter image description here

测试3:

enter image description here

这也考虑了不同的值和不同的条件。

当两个条件都为真时,使用一个public class Main { static int x = 22, y = 48; public static void main(String[] args) { oneAndBothTrue(); oneAndOneTrue(); oneAndBothFalse(); twoAndsBothTrue(); twoAndsOneTrue(); twoAndsBothFalse(); System.out.println(b); } static void oneAndBothTrue() { int value = 30; for (int i = 0; i < 2000; i++) { if (value >= x & value <= y) { doSomething(); } } } static void oneAndOneTrue() { int value = 60; for (int i = 0; i < 4000; i++) { if (value >= x & value <= y) { doSomething(); } } } static void oneAndBothFalse() { int value = 100; for (int i = 0; i < 4000; i++) { if (value >= x & value <= y) { doSomething(); } } } static void twoAndsBothTrue() { int value = 30; for (int i = 0; i < 4000; i++) { if (value >= x & value <= y) { doSomething(); } } } static void twoAndsOneTrue() { int value = 60; for (int i = 0; i < 4000; i++) { if (value >= x & value <= y) { doSomething(); } } } static void twoAndsBothFalse() { int value = 100; for (int i = 0; i < 4000; i++) { if (value >= x & value <= y) { doSomething(); } } } //I wanted to avoid print statements here as they can //affect the benchmark results. static StringBuilder b = new StringBuilder(); static int times = 0; static void doSomething(){ times++; b.append("I have run ").append(times).append(" times \n"); } } 需要更多的时间来运行,大约60%或2毫秒的时间。当其中一个或两个条件都为假时,一个&运行得更快,但它只运行约0.30-0.50毫秒。因此,在大多数情况下,&的运行速度会比&快,但性能差异仍然可以忽略不计。

答案 4 :(得分:3)

你所追求的是这样的:

x <= value & value <= y
value - x >= 0 & y - value >= 0
((value - x) | (y - value)) >= 0  // integer bit-or

有趣的是,人们几乎想看看字节码。 但很难说。我希望这是一个C问题。

答案 5 :(得分:0)

我也对答案感到好奇,所以我为此编写了以下(简单)测试:

private static final int max = 80000;
private static final int size = 100000;
private static final int x = 1500;
private static final int y = 15000;
private Random random;

@Before
public void setUp() {
    this.random = new Random();
}

@After
public void tearDown() {
    random = null;
}

@Test
public void testSingleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of single operand: " + (end - start));
}

@Test
public void testDoubleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of double operand: " + (end - start));
}

最终结果是与&amp;&amp;和总是在速度方面胜出,比&amp;大约快1.5 / 2毫秒。

修改 正如@SvetlinZarev指出的那样,我还在测量Random获取整数所需的时间。将其更改为使用预填充的随机数组,这导致单个操作数测试的持续时间大幅波动;几次运行之间的差异可达6-7ms。

答案 6 :(得分:0)

向我解释的方式是&amp;&amp;如果系列中的第一次检查为假,则返回false,而&amp;检查系列中的所有项目,无论有多少都是错误的。 I.E.

if(x> 0&amp; x&lt; = 10&amp;&amp; x

跑得比

if(x> 0&amp; x&lt; = 10&amp; x

如果x大于10,因为单个&符号将继续检查其余条件,而双&符号将在第一个非真实条件后中断。