【问题标题】:In Java, can & be faster than &&?在Java中,&可以比&&快吗?
【发布时间】:2017-01-28 00:48:08
【问题描述】:

在这段代码中:

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

value &gt;= xvalue &lt;= y 在没有特定模式的情况下为真和假时,使用&amp; 运算符会比使用&amp;&amp; 更快

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

但是虽然&gt;=&lt;= 运算符将使用简单的比较指令,但&amp;&amp; 必须涉及一个分支,并且该分支容易受到分支预测失败的影响 - 按照这个非常有名的问题:Why is it faster to process a sorted array than an unsorted array?

因此,强制表达式不包含惰性组件肯定会更具确定性,并且不易受到预测失败的影响。对吧?

注意事项:

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

更新 只是为了解释我为什么感兴趣:我一直在盯着 Martin Thompson 在他的Mechanical Sympathy blog 上写的系统,在他来和did a talk 关于 Aeron 之后。其中一个关键信息是,我们的硬件中包含所有这些神奇的东西,而我们的软件开发人员不幸地未能利用它。别担心,我不打算在我的所有代码上使用 s/&&/\&/ :-) ...但是这个网站上有很多关于通过删除分支来改进分支预测的问题,并且它发生了对我来说,条件布尔运算符是测试条件的核心

当然,@StephenC 提出了一个奇妙的观点,即把你的代码弯曲成奇怪的形状会让 JIT 更不容易发现常见的优化——如果不是现在,那么在未来。并且上面提到的非常著名的问题很特别,因为它使预测的复杂性远远超出了实际的优化。

我非常清楚,在大多数(或几乎所有)情况下,&amp;&amp; 是最清晰、最简单、最快、最好的事情 - 尽管我非常感谢发布答案的人证明了这一点!我真的很想看看在任何人的经历中是否真的有任何案例可以回答“&amp; 可以更快吗?”可能是是的...

更新 2(解决问题过于宽泛的建议。我不想对这个问题进行重大更改,因为它可能会影响下面的一些答案,这些答案质量非常好!)需要野性;这是来自Guava LongMath 类(非常感谢@maaartinus 找到这个):

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

看到第一个&amp;了吗?如果您检查链接,next 方法称为lessThanBranchFree(...),这暗示我们处于分支回避领域 - 番石榴确实被广泛使用:每个循环保存都会导致海平面下降明显。所以让我们这样提出问题:这种使用&amp;&amp;&amp; 更正常)是真正的优化吗?

【问题讨论】:

  • 如果有差异,那将是纳秒。这闻起来像是过早的优化。它为什么如此重要?真想知道,看编译后的字节码就行了。
  • @JimGarrison 这很重要,因为这样的测试通常用于比较器(即排序)和过滤器中,因此紧密循环中的数百万次执行可能很常见,然后 ns 变为 ms。此外,就&amp;&amp; 的替代方案而言,对&amp; 运算符的严格评估是Java 鲜为人知的特性,并且在多年的Java 编程中,我从未选择使用它。也许是我过于轻视了!
  • @pavlos - 我想我在问题中已经说得很清楚了(请参阅verySlowFunction() 注释);这是关于分支预测的——还是我应该进一步澄清一下?欢迎提出建议。
  • FWIW,看起来 &amp;&amp;&amp;some real uses
  • C# 编译器将生成代码,就好像你写了&amp;,即使你写了&amp;&amp;,如果它的启发式认为这样做会是一个胜利。我不知道 Java 的编译器是否也这样做,但这是一个简单的优化,如果他们没有想到它会有点令人惊讶。

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


【解决方案1】:

向我解释的方式是,如果系列中的第一个检查为假,&& 将返回假,而 & 检查系列中的所有项目,无论有多少是假的。 IE。

如果 (x>0 && x

会跑得比

如果 (x>0 & x

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

【讨论】:

  • 对不起,这没有抓住问题的重点!查看问题中的第一个“注释”-我对此非常明确。显然,如果不执行后续条件可以节省大量时间,那很好,我们都知道。但是要做到这一点涉及到一个分支,并且现代处理器指令管道有时会猜测分支将采用的方向,结果是a)错误和b)相当昂贵。请阅读我链接到的(非常著名的)问题的最佳答案,然后决定是否要保留此答案。
【解决方案2】:

好的,所以你想知道它在较低级别的行为......那么让我们看看字节码!

编辑:最后添加了为 AMD64 生成的汇编代码。看看一些有趣的注释。
EDIT 2(re: OP's "Update 2"):也为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 (&amp;&amp;) 方法按预期生成两个条件跳转:

  1. 它将valuex 加载到堆栈中,如果value 较低,则跳转到L1。否则它会继续运行下一行。
  2. 它将valuey 加载到堆栈中,如果value 更大,它也会跳转到L1。否则它会继续运行下一行。
  3. 这恰好是 return true,以防两次跳转均未完成。
  4. 然后我们将标记为 L1 的行标记为 return false

但是,AndNonSC (&amp;) 方法会生成 三个 条件跳转!

  1. 它将valuex 加载到堆栈中,如果value 较低,则跳转到L1。因为现在它需要保存结果来与AND的其他部分进行比较,所以它必须执行“save true”或“save false”,它不能用相同的指令同时执行。
  2. 它将valuey 加载到堆栈中,如果value 更大,则跳转到L1。再次需要保存truefalse,根据比较结果,这是两条不同的行。
  3. 现在两个比较都完成了,代码实际上执行了AND操作——如果两者都为真,它会跳转(第三次)返回真;否则它会继续执行到下一行以返回 false。

(初步)结论

虽然我对 Java 字节码的经验不是很丰富,而且我可能忽略了一些事情,但在我看来,&amp; 在任何情况下实际上都会比&amp;&amp; 执行更糟糕:它生成更多的指令来执行,包括更多的条件跳转来预测和可能失败。

按照其他人的建议,重写代码以用算术运算替换比较可能是使&amp; 成为更好选择的一种方法,但代价是使代码变得不那么清晰。
恕我直言,对于 99% 的场景来说,这是不值得的麻烦(不过,对于需要极度优化的 1% 循环来说,这可能是非常值得的)。

编辑:AMD64 程序集

正如 cmets 中所述,相同的 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 选项

  # {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 代码会根据我们选择默认 AT&T 语法还是 Intel 语法而有所不同。
  • 使用 AT&T 语法:
    • AndSC 方法的 ASM 代码实际上更长,每个字节码 IF_ICMP* 都转换为两条汇编跳转指令,总共有 4 次条件跳转。
    • 同时,对于AndNonSC 方法,编译器会生成更直接的代码,其中每个字节码IF_ICMP* 仅转换为一条汇编跳转指令,保持原来的3 个条件跳转计数。
  • 使用 Intel 语法:
    • AndSC 的 ASM 代码更短,只有 2 个条件跳转(不包括最后的非条件 jmp)。实际上它只是两个 CMP、两个 JL/E 和一个 XOR/MOV,具体取决于结果。
    • AndNonSC 的 ASM 代码现在比 AndSC 长! 然而,它只有一个条件跳转(第一次比较),使用寄存器直接比较第一个结果和第二个结果,没有更多的跳转。

ASM代码分析后的结论

  • 在 AMD64 机器语言级别,&amp; 运算符似乎生成的 ASM 代码具有较少的条件跳转,这对于高预测失败率可能更好(例如随机 values)。
  • 另一方面,&amp;&amp; 运算符似乎生成的 ASM 代码指令较少(无论如何都使用 -XX:PrintAssemblyOptions=intel 选项),这对于预测友好的 非常长 循环可能更好输入,每次比较的 CPU 周期数越少,从长远来看会产生影响。

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


附录:Guava 的isPowerOfTwo 方法

在这里,Guava 的开发人员提出了一种计算给定数字是否为 2 的幂的简洁方法:

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

引用 OP:

这种使用&amp;&amp;&amp; 更正常)是真正的优化吗?

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

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

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

Guava 版本的英特尔 ASM 代码

  # {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    

&amp;&amp; 版本的 Intel 汇编代码

  # {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    

在这个具体示例中,JIT 编译器为&amp;&amp; 版本生成的汇编代码 少于为 Guava 的&amp; 版本生成的汇编代码(而且,在昨天的结果之后,我真的对此感到惊讶) .
与 Guava 相比,&amp;&amp; 版本转换为 JIT 编译的字节码减少了 25%,汇编指令减少了 50%,并且只有两个条件跳转(&amp; 版本有四个)。

所以一切都表明 Guava 的 &amp; 方法效率低于更“自然”的 &amp;&amp; 版本。

...或者是吗?

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

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 会怎样

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
  .....

惊喜! Java 7 中 JIT 编译器为 &amp; 方法生成的汇编代码现在只有 一个 条件跳转,而且更短!而&amp;&amp; 方法(你必须相信我,我不想弄乱结尾!)保持不变,只有两个条件跳转和几个指令,最重要的是。
毕竟,Guava 的工程师似乎知道他们在做什么! (如果他们试图优化 Java 7 的执行时间,那就是 ;-)

回到 OP 的最新问题:

这种使用&amp;&amp;&amp; 更正常)是真正的优化吗?

恕我直言,答案是一样的,即使对于这个(非常!)特定场景:这取决于您的 JVM 实现、编译器、CPU 和输入数据.

【讨论】:

  • 嗯,Java 字节码是最接近 ASM 的东西,然后才深入了解每个操作系统和 CPU 的细节。当然,IBM javac 可能会输出与官方 Oracle 或 OpenJDK 不同的代码……当然,X86 机器中的机器代码可能与 PowerPC AIX 系统或许多智能手机中使用的 Snapdragon CPU 不同——每个平台都有自己的编译器和优化。但在这样一个简单的情况下,我怀疑一个 CPU 到另一个 CPU 的差异会比 2 对 3 个字节码条件跳转产生更大的差异。
  • 虽然它可能是“最接近 ASM 的东西”,但它还不足以让您得出任何合乎逻辑的结论。简而言之,代码经过 JIT 编译后,JVM 不会执行字节码。
  • @walen 你把它清理干净了。您最初说的是跳转而不是条件跳转(这实际上是一个分支)。只有一个地方可以跳,所以没有什么可以预测的。因此不可能有误判。
  • @Riley 是的,但我可以理解,所以没问题 :) 请允许我引用 Intel 的官方 Intel ® 64 and IA-32 Architectures Software Developer’s Manual:“5.1.7 控制转移指令 控制转移指令提供跳转、条件跳转、循环、调用和返回操作来控制程序流程。"
  • 嗯,我认为这是一个很棒的答案。 Java8 中可能存在一些微妙之处,这可能使其在 HotSpot 魔术或其他东西的基础上应用进一步的优化。在这种情况下,可能会产生一个新问题......同时,很好的一个!非常感谢!
【解决方案3】:

对于这类问题,您应该运行微基准测试。我在这个测试中使用了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 - 类似于 &amp;,但仅当其左侧操作数的值为 true
bitwise OR 时才计算其右侧操作数 -结果值是操作数值的按位或运算

【讨论】:

  • 这是迄今为止最好的基准,但它也有缺陷:)黑洞比 && 或 & 花费更多的时间,所以你基本上是在测量黑洞的性能 :) 尝试使用像消耗(a & b & c 7 d & f & g ....&z);
  • @SusanW 顺便说一句,是 JMH bug 帮助发现 HotSpot 确实缩短了 &amp; 的评估。所以,回答最初的问题 - 不,JVM 仍然为&amp; 生成一个条件分支。
  • @SusanW @SubOptimal 我编辑了我的答案以包含实际的 JIT 生成的 ASM 代码。看起来&amp; 可能在某些情况下会更好!欢迎评论:-)
  • @SusanW 不,methodWithSideEffects() 不会被跳过,否则将违反规范。但是,在这种情况下,可以优化出没有副作用的方法。
  • 关于非快捷逻辑运算符的含义已经存在很多混淆。您能否修改这篇文章,以免将它们称为按位?您的测试中没有按位计算。
【解决方案4】:

我将从不同的角度来解决这个问题。

考虑这两个代码片段,

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

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

如果我们假设valuexy 具有原始类型,那么这两个(部分)语句将对所有可能的输入值给出相同的结果。 (如果涉及包装器类型,则它们不完全等效,因为对 y 的隐式 null 测试可能在 &amp; 版本而不是 &amp;&amp; 版本中失败。)

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

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

  • 如果不是,那么在源代码级别使用哪个版本都没有关系。

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

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

  • 确实,如果您以一种对此进行优化的方式编写 Java 代码,则有机会选择更“晦涩”的代码版本,您可能 抑制当前或未来 JIT 编译器的优化能力。

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

1 - 我不认为这是任何证据。

2 - 除非您是真正编写 Java JIT 编译器的小社区之一...


“非常著名的问题”在两个方面很有趣:

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

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

【讨论】:

  • 您关于抑制未来优化的观点做得很好! - 故意将'&'置于条件中,无异于“为了欺骗系统而没有明确表达意图”,并且当你对你的电脑撒谎时,它会报复 ... .
  • 哪个更快取决于数据。这是 JIT 无法知道的。或者 JVM JIT 可以分析这样的事情吗?在那种情况下,这将是完全可行的。
  • 是的。 JIT 可以做到这一点。 HotSpot JIT 编译器会在字节码被解释之前的阶段......在编译之前这样做。
  • 如果xy 是常量或可预测的值,优化后的代码将更像value-x ≤ͧ y-x,其中≤ͧunsigned long 比较,y-x 是常量,尽管即使xy 是不可预测的,但如果两个分支被认为比急切执行的比较更昂贵(数字比较与减号操作相当),则可以使用单个比较变体。所以考虑&amp;&amp;&amp; 确实没有意义。
  • 未来的优化 - 喜欢这方面。考虑一下“a+b+c”是如何演变成使用 StringBuffers 的,即使它们可能并不那么重要。然后,当 StringBuilders 出现时,人们有了这些庞大的线程安全 StringBuffers,这样的开销是不必要的。现在 "a+b+c" 在编译时转换为 StringBuilders,但由于过度优化,任何显式的 StringBuffers 显然仍然存在。
【解决方案5】:

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

使用&amp; 而不是&amp;&amp; 来节省一纳秒,如果在一些非常罕见的情况下没有意义,那么您已经浪费了更多时间来考虑差异,而不是使用&amp; 而不是&amp;&amp;

编辑

我很好奇,决定做一些基准测试。

我做了这门课:

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){

        }
    }
}

并使用 NetBeans 运行了一些分析测试。我没有使用任何打印语句来节省处理时间,只知道两者都评估为true

第一次测试:

第二次测试:

第三次测试:

从分析测试中可以看出,与使用两个 &amp;&amp; 相比,仅使用一个 &amp; 实际运行时间要长 2-3 倍。这确实有点奇怪,因为我确实期望只有一个 &amp; 有更好的性能。

我不是 100% 确定为什么。在这两种情况下,都必须计算两个表达式,因为它们都是真的。我怀疑 JVM 在后台做了一些特殊的优化来加速它。

故事的寓意:约定好,过早优化不好。


编辑 2

我在考虑@SvetlinZarev 的 cmets 和其他一些改进的情况下重新编写了基准代码。这是修改后的基准代码:

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");
    }
}

以下是性能测试:

测试 1:

测试 2:

测试 3:

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

当两个条件都为真时,使用一个&amp; 需要更多时间来运行,大约多出 60% 或 2 毫秒的时间。当一个或两个条件都为假时,一个&amp; 运行得更快,但它的运行速度只快了大约 0.30-0.50 毫秒。所以&amp; 在大多数情况下会比&amp;&amp; 运行得更快,但性能差异仍然可以忽略不计。

【讨论】:

  • 您的微基准测试完全有缺陷。 JIT 会优化掉那些空的 for 循环,更不用说像在你的代码中那样执行一次方法永远不会给出任何有意义的结果。
  • 感谢您指出这一点,我会考虑到这一点重做测试。
  • 唯一正确的微基准测试方法是使用像 JMH 这样的工具。
  • 除非您在一台非常旧的机器上运行,否则您的循环不会执行足够多的时间来获得任何有意义的结果。此外,您调用事物的顺序也会产生巨大的差异。最后,如果您继续追加到 StringBuilder,它最终将需要分配大量内存,这将需要很长时间。
  • 'BothFalse' 无效。那些 100 的方法测试与 60 相同的东西。你不能同时低于范围和高于范围,所以 BothFalse 是无法实现的..
【解决方案6】:

我也很好奇这个答案,所以我为此写了以下(简单)测试:

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));
}

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

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

【讨论】:

  • 好吧,有趣:我可以看到第一个条件大部分会成功(generated &gt;= x),这意味着预测器通常会正确处理(如果它按照我认为的方式工作)。我将尝试摆弄那些 'x' 和 'y' 值 - 我认为 x=40000y=60000 会很有趣(每次测试成功率 50%)。
  • 有了这些值,&& 仍然胜过 &。这次两者之间的平均差异似乎也更高了,从未低于 2 毫秒,有时甚至超过 3 毫秒。
  • 您正在测量random.nextInt(),因为它比简单的 && 或 & 花费更多的时间。你的测试有缺陷
  • @SvetlinZarev 关于随机评论的好点;我已将其更改为使用填充随机整数的数组,最终结果相同的是 && 比 & 快。
  • @Oromë 你仍然缺乏热身 :)
【解决方案7】:

你所追求的是这样的:

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

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

【讨论】:

    猜你喜欢
    • 2020-01-15
    • 1970-01-01
    • 2018-05-19
    • 2012-09-16
    • 2011-04-11
    • 2015-08-12
    • 2013-10-30
    • 2011-06-17
    • 2021-03-26
    相关资源
    最近更新 更多