在Java中,&可以比&&快吗?


72

在此代码中:

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

在没有特定模式的情况下,何时value >= xvalue <= y可能与否一样,使用&运算符会比使用&&

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

但是,尽管>=or<=运算符将使用一个简单的比较指令,但&&必须包含一个分支,并且该分支易受分支预测失败的影响-正如这个非常著名的问题:为什么处理排序数组要比未排序数组快?

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

笔记:

  • 如果代码看起来像这样,显然我的问题的答案将是“if(value >= x && verySlowFunction())。我专注于“足够简单”的RHS表达式。
  • 无论如何,那里有一个条件分支(该if语句)。我无法向自己证明这是无关紧要的,替代性表述可能是更好的示例,例如boolean b = value >= x && value <= y;
  • 所有这些都属于可怕的微观优化领域。是的,我知道:-) ...不过有趣吗?

更新 只是为了解释我为什么感兴趣:我一直在盯着Martin Thompson在他来关于Aeron的话题之后在他的Mechanical Sympathy博客上一直在写的系统。关键信息之一是我们的硬件中包含了所有这些神奇的内容,而我们的软件开发人员却不幸地无法利用它。不用担心,我不会在所有代码上都使用s / && / \&// :-) ...但是这个站点上有很多关于通过删除分支来改善分支预测的问题,对我来说,条件布尔运算符是测试条件的核心

当然,@ StephenC提出了一个奇妙的观点,即将代码弯曲为怪异的形状可以使JIT不太容易发现常见的优化-如果不是现在,则不是将来。而且上述非常著名的问题之所以特别,是因为它使预测的复杂性远远超出了实际优化的范围。

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

更新2:( 提出有关该问题过于广泛的建议。我不想对此问题进行重大更改,因为它可能会损害下面的某些回答,这些回答的质量非常高!)也许是一个普遍的例子对于; 这是来自Guava LongMath班的(非常感谢@maaartinus找到了这个):

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

首先看到那个&吗?并且,如果您检查链接,则下一个方法称为lessThanBranchFree(...),这表明我们处于避开分支的领域-番石榴确实得到了广泛使用:每个保存的周期都会导致海平面明显下降。因此,让我们以这种方式提出问题:这种使用&(在哪里&&会更正常)是真正的优化?


16
如果存在差异,则将是十亿分之一秒。这闻起来像过早的优化。它为什么如此重要?如果您真的想知道,只需查看编译后的字节码即可。
Jim Garrison

6
@JimGarrison这很重要,因为类似的测试通常用于比较器(即排序)和过滤器中,因此紧密循环中的数百万次执行可能很常见,然后ns变为ms。同样,就&Java的替代品而言,对运算符的严格评估是Java鲜为人知的特性&&,而且在Java编程的多年中,我从未选择使用它。也许我太不屑一顾了!
SusanW

2
@pavlos-我以为我在问题中很清楚了(请参阅verySlowFunction()注释);这是关于分支预测的-或者我应该再澄清一下吗?欢迎提出建议。
SusanW

3
FWIW,它看起来像&&&一些真正的用途
maaartinus

5
即使您编写了C#编译器,&如果您&&的试探法认为这样做会是一次胜利,那么即使您编写了C#编译器,它也会像您编写的那样生成代码。我不知道Java的编译器是否也可以这样做,但是这是一个简单的优化,如果他们没有想到的话,这会有点令人惊讶。
埃里克·利珀特

Answers:


75

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

编辑:最后添加了生成的AMD64汇编代码。看一些有趣的笔记。
编辑2(re:OP的“ Update 2”):也为番石榴的isPowerOfTwo方法添加了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&&)方法生成2个条件跳转,如所预期:

  1. 它加载valuex压入堆栈,并且如果跳跃到L1value更低。否则,它将继续运行下一行。
  2. 它加载valuey堆叠到堆栈上,如果value更大则跳到L1 。否则,它将继续运行下一行。
  3. 如果这return true两个跳跃都没有发生,则可能是这种情况。
  4. 然后,我们将标记为L1的线称为return false

AndNonSC&)方法,但是,产生3个条件跳转!

  1. 它加载valuex压入堆栈,如果跳跃到L1value更低。因为现在它需要保存结果才能将其与AND的其他部分进行比较,所以它必须执行“保存true”或“保存false”,因此不能对同一条指令执行全部操作。
  2. 它加载valuey堆栈,如果value更大则跳到L1 。再次需要保存truefalse,这是两条不同的线,具体取决于比较结果。
  3. 现在完成了两个比较,代码实际上执行了AND操作-如果两个都为真,则它会跳(第三次)以返回true;否则它将继续执行到下一行以返回false。

(初步)结论

尽管我对Java字节码的了解不是很丰富,并且我可能忽略了一些东西,但在我看来,它的&实际性能要比每种情况都要&&:它会生成更多的指令来执行,包括更多的条件跳转来预测并可能失败。

如其他人所建议的那样,重写代码以用算术运算代替比较可能是做出&更好选择的一种方法,但是这样做的代价是使代码变得不太清晰。
恕我直言,这对于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选项的方法

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

ASM代码分析后的结论

  • 在AMD64机器语言级别上,&操作员似乎会生成带有较少条件跳转的ASM代码,这对于较高的预测失败率(value例如,随机数s)可能更好。
  • 另一方面,&&操作员似乎使用较少的指令(-XX:PrintAssemblyOptions=intel无论如何都带有该选项)来生成ASM代码,这对于具有预测友好输入的真正较长的循环可能会更好,在这种情况下,每次比较所需的CPU周期数更少,可以有所作为长期来说。

正如我在一些评论中所述,这在系统之间会有很大的不同,因此,如果我们谈论的是分支预测优化,唯一的真实答案是:它取决于您的JVM实现,您的编译器,您的CPU和您的输入数据


附录:番石榴的isPowerOfTwo方法

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

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

引用OP:

&(在&&更正常的地方)这种使用是真正的优化吗?

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

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

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

番石榴版本的英特尔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    

英特尔的ASM&&版本代码

  # {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编译器生成少的汇编代码&&的版本比番石榴的&版本(和,经过昨日的结果,我诚实地感到意外)。
与Guava相比,该&&版本将JIT编译的字节码减少了25%,将汇编指令减少了50%,并且仅执行了两次条件跳转(该&版本有四个条件跳转)。

因此,一切都表明番石榴的&方法比更“自然”的&&版本效率低。

……还是?

如前所述,我正在使用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编译器为该方法生成的汇编代码现在只有一个条件跳转,而且更短!尽管&&方法(您必须在这一点上相信我,但我不想弄乱结局!)仍然是相同的,只有两次条件跳转和更少的指令(顶部)。
看来番石榴的工程师毕竟知道他们在做什么!(如果他们试图优化Java 7执行时间,则为;-)

回到OP的最新问题:

&(在&&更正常的地方)这种使用是真正的优化吗?

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


8
嗯,在讨论每个OS和CPU的细节之前,Java字节码是最接近ASM的东西。当然,IBMjavac可能会输出与官方Oracle或OpenJDK不同的代码...当然,X86机器中的机器代码可能与PowerPC AIX系统或许多智能手机中使用的Snapdragon CPU有所不同-每个平台都会有自己的编译器和优化程序。但是在这样一个简单的情况下,我怀疑从一个CPU到另一个CPU的差异是否会比2和3个字节码的条件跳转产生更大的差异。
2016年

9
尽管它可能是“最接近ASM的东西”,但它还不够接近,无法让您得出任何合乎逻辑的结论。简而言之,在对代码进行JIT编译之后,JVM不会执行字节码。
史蒂芬·C

1
@walen您清除了它。您最初说的是跳转,而不是条件跳转(实际上是分支)。只有一个地方可以跳,所以没有什么可以预测的。因此,不会有错误的预测。
莱利

2
@Riley是的,但是我可以联系,所以没有问题:)请允许我引用英特尔官方的Intel®64和IA-32体系结构软件开发人员手册:“ 5.1.7控制传递指令 控制传递指令提供跳转和条件跳转。,循环以及调用和返回操作来控制程序流。
walen

2
好吧,我认为这是一个了不起的答案。Java8中可能有一些细微之处,这可能会使它在HotSpot魔术之类的东西的基础上应用进一步的优化。在这种情况下,可能会产生一个新问题……同时,一个好问题!非常感谢你!
SusanW

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.08617.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.2407.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.84715.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.87711.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.74315.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.52416.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.19019.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.09415.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.9135.537  ops/us

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

一些参考:

布尔逻辑AND-结果值为true两个操作数都为true; 否则,结果为false
条件AND-类似于&,但是仅当其左侧操作数的值是true
按位OR时才求值其右侧操作数-结果值是操作数值的按位异或


4
到目前为止,这是最好的基准,但是它也存在缺陷:)黑洞比&&或&花费更多的时间,因此您基本上是在测量黑洞的性能:)尝试使用消耗(a&b&c 7 d&f &g ....&z);
Svetlin Zarev '16

1
@SusanW顺便说一句,正是JMH错误帮助发现HotSpot确实简化了的评估&。因此,回答原始问题-否,JVM仍然为生成条件分支&
apangin

1
@SusanW @SubOptimal我编辑了答案,以包含实际的JIT生成的ASM代码。在某些情况下,& 可能看起来更好!评论欢迎:-)
瓦伦

1
@SusanW不,methodWithSideEffects()不会被跳过,否则将违反规范。但是,在这种情况下,可以优化无副作用的方法。
apangin '16

1
非快捷逻辑运算符的含义已经引起很多混乱。能否请您修改此帖子,以免按位引用它们?测试中没有按位计算。
JimmyJames

13

我将从另一个角度出发。

考虑这两个代码片段,

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

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

如果我们假设valuexy有一个基本类型,那么这两个(部分)语句可以得到同样的结果对于所有可能的输入值。(如果涉及包装器类型,则它们不是完全等效的,因为对此的隐式null测试y可能会在&版本中而不是&&版本中失败。)

如果JIT编译器做得很好,则其优化器将能够推断出这两个语句具有相同的作用:

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

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

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

  • 如果当前的JIT编译器(在任何给定的平台上)不能很好地进行优化以解决此问题,则下一代可以很好地进行……取决于经验证据是否表明这是一种值得优化的模式。

  • 确实,如果您以对此进行优化的方式编写Java代码,则有可能通过选择代码的“模糊”版本来抑制当前或将来的JIT编译器的优化能力。

简而言之,我不认为您应该在源代码级别上进行这种微优化。并且,如果您接受此论点1,并遵循其逻辑结论,那么哪个版本更快的问题就变成了...争论2

1-我不认为这几乎是证明。

2-除非您是实际编写Java JIT编译器的人中的一小部分,否则...


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

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

  • 另一方面,排序数组不一定是正确的事情……只是因为排序后的数组可以更快地处理。对数组进行排序的成本可能会比节省的成本高很多。


3
关于禁止将来的优化的观点非常好!-故意将'&'置于条件中等同于“未能清楚地表达意图以欺骗系统”,并且当您躺在计算机上时,它将报仇..
SusanW 2016年

哪一个更快取决于数据。这是JIT所不知道的。还是JVM JIT可以描述这样的事情?在这种情况下,这将是完全可行的。
usr

是。JIT可以做到这一点。HotSpot JIT编译器会在编译之前解释字节码之前的阶段执行此操作。
史蒂芬·C

如果xy可以是常量或可预测值,优化代码,而像value-x ≤ͧ y-x这里≤ͧunsigned long比较y-x恒定的,但即使xy不可预测,也比较单一变异可能被使用,如果两个分支被认为比一个期待更贵执行比较(数值比较与减号运算相当)。因此,思考&&&确实没有任何意义。
Holger

1
未来的优化-喜欢这一方面。考虑一下“ a + b + c”如何变成使用StringBuffers,即使它们实际上并没有那么重要。然后当StringBuilders出现时,现在人们有了这些笨重的线程安全的StringBuffers,而这些开销是不必要的。现在,“ a + b + c”在编译时会调整为StringBuilders,但是由于狂热的过度优化,显然任何明显的StringBuffer仍然存在。
corsiKa

6

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

使用&&&保存纳秒,如果是在一些非常罕见的情况下,是没有意义的,你已经浪费了更多的时间考虑比你已经使用保存的差异&&&

编辑

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

我上了这堂课:

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

第一次测试:

第一次分析测试

第二次测试:

第二次分析测试

第三次测试:

第三次剖析测试

正如您通过性能分析测试所看到的,&与使用两次相比,仅使用一次实际上要花费2-3倍的时间&&。这确实和我所期望的只有一个更好的表现有些奇怪&

我不确定100%为什么。在这两种情况下,都必须对两个表达式都进行求值,因为两者都是正确的。我怀疑JVM在后台进行了一些特殊的优化以加快速度。

这个故事的寓意是:惯例是好的,过早的优化是不好的。


编辑2

我谨记@SvetlinZarev的评论和其他一些改进来重做基准代码。这是修改后的基准代码:

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:

在此处输入图片说明

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

&当两个条件都满足时,使用一个会花费更多时间,大约要多60%或2毫秒。当一个或两个条件都为假时,则&运行速度会更快,但运行速度只会加快约0.30-0.50毫秒。因此&它将比&&大多数情况下运行得更快,但是性能差异仍然可以忽略不计。


5
您的微型基准测试完全有缺陷。JIT将优化那些空的for循环,更不用说像在代码中那样对方法的单次执行永远不会给出任何有意义的结果。
Svetlin Zarev '16

1
感谢您指出这一点,我将牢记这一点重做测试。
路加·梅拉亚

4
微基准测试的唯一正确方法是使用JMH之类的工具。
Svetlin Zarev '16

除非您在真正的旧机器上运行,否则循环执行的时间不足以获取任何有意义的结果。同样,呼叫时的顺序也会产生很大的变化。最后,如果您继续附加到StringBuilder,则最终将需要分配大量内存,这将需要很长时间。
JimmyJames

“ BothFalse”无效。与100个测试的那些方法同样的事情60.你不能同时低于该范围及以上在同一时间范围内,所以BothFalse是无法实现..
辛格

3

您所追求的是这样的:

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

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


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

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

编辑: 正如@SvetlinZarev所指出的,我也在测量Random获得一个整数所花费的时间。更改为使用预填充的随机数数组,这导致单个操作数测试的持续时间剧烈波动;几次运行之间的差异最大为6-7毫秒。


好吧,有趣:我可以看到第一个条件通常会成功(generated >= x),这意味着预测变量通常会正确处理(如果它按我认为的方式工作)。我要去尝试与“X”和“Y”值摆弄-我想x=40000y=60000会很有趣(每个测试50%成功)。
SusanW

通过这些值,&&仍然胜过&。这次两者之间的平均差异似乎也更高,从不低于2ms,有时甚至高于3ms。
Oromë

5
您正在测量random.nextInt()它,因为它比简单的&&或&要花费更多的时间。您的测试有缺陷
Svetlin Zarev

1
@SvetlinZarev关于随机评论的要点;我将其更改为使用填充有随机整数的数组,最终结果是&&比&快。
Oromë

2
@Oromë您仍然缺少热身运动:)
Svetlin Zarev

0

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

如果(x> 0 && x <= 10 && x

将比

如果(x> 0&x <= 10&x

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


抱歉,这没有回答问题的重点!看问题中的第一个“ Note”-我对此非常清楚。显然,如果不执行后续条件可以节省大量时间,那么很好,我们都知道这一点。但是要做到这一点涉及分支,现代处理器指令流水线有时会猜测分支的方向,这将导致a)错误和b)相当昂贵。请阅读我链接到的(非常著名的)问题的最佳答案,然后决定是否要保留此答案。
SusanW
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.