在Java中,可以比&&更快吗?
在这个代码中:
if (value >= x && value <= y) {
当value >= x
和value <= y
情况下, 如果没有特定的模式可能是真的, 那么使用&
运算符会比使用&&
更快吗?
具体来说,我正在思考如何&&
懒惰地评估右侧expression式(即只有当LHS是真实的),这意味着一个条件,而在Java &
在这方面保证严格评估两个(布尔)子expression式。 价值结果是相同的任何方式。
但是,当>=
或<=
运算符将使用一个简单的比较指令时, &&
必须包含一个分支,并且该分支容易出现分支预测失败 – 按照这个非常着名的问题: 为什么处理sorting数组的速度比一个未分类的数组?
因此,强迫expression没有懒惰的组件肯定会更确定,不容易被预测失败。 对?
笔记:
- 显然,如果代码如下所示,我的问题的答案是否定的:
if(value >= x && verySlowFunction())
。 我正在关注“足够简单”的RHSexpression式。 - 那里有一个条件分支(
if
语句)。 我无法向自己certificate这是无关紧要的,而替代forms可能是更好的例子,比如boolean b = value >= x && value <= y;
- 这一切都落入了可怕的微观优化的世界。 是的,我知道:-) …虽然有趣?
更新只是为了解释我为什么感兴趣:我一直在盯着Martin Thompson在他的Mechanical Sympathy博客上写过的系统,在他来和Aeron 谈论之后 。 其中一个关键信息就是我们的硬件中包含了所有这些神奇的东西,而我们的软件开发人员却无法利用它。 别担心,我不打算在所有的代码上使用s / && / \&/ … …但是在这个网站上有很多关于通过删除分支来改进分支预测的问题,对我来说,条件布尔运算符是testing条件的核心 。
当然,@StephenC提供了一个奇妙的观点,即将代码弯曲成奇怪的形状,可以让JIT轻松地发现常见的优化 – 即使不是现在,也是未来。 而且上面提到的非常着名的问题是非常特殊的,因为它将预测的复杂性推到了实际的优化之外。
我很清楚在大多数(或者几乎所有 )情况下, &&
是最简单,最快,最好的事情,尽pipe我非常感谢已经发布答案的人们展示了这一点! 我真的很感兴趣,看看有没有人遇到过“可以更快?”的答案。 可能是 …
更新2 🙁 解决这个问题过于宽泛的build议,我不想对这个问题做出重大改变,因为它可能会影响下面的一些答案,这些答案具有特殊的质量!)也许一个野外的例子叫做对于; 这是从Guava LongMath类(非常感谢@maaartinusfind这个):
public static boolean isPowerOfTwo(long x) { return x > 0 & (x & (x - 1)) == 0; }
首先看到? 如果你检查链接, 下一个方法被称为lessThanBranchFree(...)
,这暗示我们在分支回避领土 – 番石榴是真正广泛使用:每个周期的保存导致海平面下降明显。 那么让我们把这个问题放在这个问题上: 这个&
(其中&&
会更加正常)是一个真正的优化?
好吧,所以你想知道它是如何在较低层次上运行的…让我们来看看字节码吧!
编辑:在最后添加生成的AMD64的汇编代码。 看看一些有趣的笔记。
编辑2(重新:OP的“更新2”):增加了Guava的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运算符的types。
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 III] [II] 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
( &&
)方法会生成两个条件跳转:
- 它将
value
和x
加载到堆栈上,如果value
较低,则跳转到L1。 否则它会继续运行下一行。 - 它将
value
和y
加载到堆栈上,并且如果value
更大,则跳转到L1。 否则它会继续运行下一行。 - 如果两跳都没有发生,那么碰巧
return true
。 - 然后我们有标记为L1的行,这是一个
return false
。
然而, AndNonSC
( &
)方法会生成三个条件跳转!
- 它将
value
和x
加载到堆栈上,如果value
较低,则跳转到L1。 因为现在需要保存结果与AND的其他部分进行比较,所以必须执行“savetrue
”或“savefalse
”,不能同时执行这两个指令。 - 它将
value
和y
加载到堆栈上并在value
更大时跳转到L1。 再一次需要保存true
或false
,根据比较结果,这是两条不同的线。 - 现在这两个比较都完成了,代码实际上执行AND操作 – 如果两者都是真的,它会跳转(第三次)返回true; 否则它继续执行到下一行返回false。
(初步)结论
虽然我对Java字节码的使用经验并不是很多,但我可能忽略了一些东西,在我看来,在任何情况下,实际上都会比&&
更糟糕 :它会生成更多的指令来执行,包括更多的条件跳转来预测和失败。
像其他人提出的那样,用算术运算代替比较的代码可能是一种更好的select方式,但代价却不那么明显。
恕我直言,它是不值得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
- 首先,生成的ASM代码根据我们select默认AT&T语法还是Intel语法而有所不同。
- 使用AT&T语法:
- 对于
AndSC
方法,ASM代码实际上更长 ,每个字节IF_ICMP*
转换为两个汇编跳转指令,总共4个条件跳转。 - 同时,对于
AndNonSC
方法,编译器生成一个更直接的代码,其中每个字节码IF_ICMP*
仅被转换为一个汇编跳转指令,保持3个条件跳转的原始计数。
- 对于
- 使用英特尔语法:
-
AndSC
的ASM代码较短,只有两个条件跳转(不包括最后的非条件jmp
)。 实际上这只是两个CMP,两个JL / E和一个XOR / MOV,具体取决于结果。 -
AndNonSC
的ASM代码现在比AndSC
更长! 但是 ,它只有一个条件跳转(用于第一个比较),使用寄存器直接比较第一个结果和第二个,没有更多的跳转。
-
ASM代码分析后的结论
- 在AMD64机器语言层面,
&
运算符似乎以更less的条件跳转生成ASM代码,这对于高预测失败率(例如随机value
s)可能更好。 - 另一方面,
&&
运算符似乎用更less的指令生成ASM代码(无论如何,使用-XX:PrintAssemblyOptions=intel
选项),这对于预测友好的input的真正长循环可能更好,其中CPU周期数较less对于每一个比较可以在长远来看有所作为。
正如我在一些评论中所指出的那样,这在系统之间会有很大的差异,所以如果我们谈论分支预测优化,唯一真正的答案就是: 它取决于你的JVM实现,你的编译器,CPU以及你的input数据 。
附录:番石榴的isPowerOfTwo
方法
在这里,番石榴的开发人员提出了一个简洁的方式来计算给定的数字是否是2的幂:
public static boolean isPowerOfTwo(long x) { return x > 0 & (x & (x - 1)) == 0; }
引用OP:
这是
&
使用(&&
将更加正常)一个真正的优化?
为了弄清楚是否是这样,我在testing类中添加了两个类似的方法:
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的&
版本less得多(昨天的结果之后,我真的很惊讶)。
与Guava相比, &&
版本将JIT编译的字节码减less了25%,汇编指令减less了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 .....
惊喜! JIT编译器在Java 7中为&
方法生成的汇编代码现在只有一个条件跳转,而且更短! 而&&
方法(你必须相信我这一个,我不想混乱的结局!)仍然大致相同,有两个有条件的跳转和less一些指令,上衣。
毕竟,看起来番石榴的工程师们知道他们在干什么! (如果他们试图优化Java 7的执行时间,那是;-)
所以回到OP的最新问题:
这是
&
使用(&&
将更加正常)一个真正的优化?
恕我直言,答案是相同的 ,甚至对于这个(非常!)具体的情况: 它取决于你的JVM实现,你的编译器,你的CPU和你的input数据 。
对于这样的问题,你应该运行一个微基准。 我使用JMH进行这个testing。
基准执行为
// 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
根据基准名称。
吞吐量基准testing的结果(五次预热和十次测量迭代)是:
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
评估本身的结果并没有那么不同。 只要这段代码没有发现性能影响,我就不会尝试优化它。 根据代码中的位置,热点编译器可能决定做一些优化。 上面的基准可能没有涵盖。
一些参考:
布尔逻辑与 – 如果两个操作数值都为true
则结果值为true
; 否则,结果是false
条件AND与 – 相似,但只有在其左侧操作数的值为true
时才计算其右侧操作数
按位OR – 结果值是操作数值的按位或(OR)
我将从另一个angular度来看这个。
考虑这两个代码片段,
if (value >= x && value <= y) {
和
if (value >= x & value <= y) {
如果我们假设这个value
, x
, y
有一个原始types,那么这两个(部分)语句将会为所有可能的input值给出相同的结果。 (如果包含了包装types,那么它们就不完全等价,因为对于y
可能会失败,而不是&&
版本,所以对y
进行隐式null
testing。
如果JIT编译器做得很好,它的优化器将能够推断出这两个语句做同样的事情:
-
如果一个比另一个更可预测的话,那么它应该能够在JIT编译的代码中使用更快的版本。
-
如果不是,那么在源代码级使用哪个版本并不重要。
-
由于JIT编译器在编译之前收集path统计信息,因此可能会有更多关于程序员(!)的执行特性的信息。
-
如果当前这一代JIT编译器(在任何给定的平台上)没有得到足够的优化来处理这个问题,那么下一代很可能会做…取决于经validation据是否指出这是一个值得优化的模式。
-
事实上,如果您以某种方式为您编写Java代码,则有可能通过select更加“不明确”的代码版本来抑制当前或未来的JIT编译器的优化能力。
总之,我不认为你应该在源代码级进行这种微型优化。 如果你接受这个论点1 ,并按照它的逻辑结论,哪个版本更快的问题是…争议2 。
1 – 我不认为这是一个certificate。
2 – 除非你是真正编写Java JIT编译器的人之一,
“非常着名的问题”在两个方面是有趣的:
-
一方面,这是一个例子,其中所需的优化types超出了JIT编译器的能力。
-
另一方面,对数组sorting不一定是正确的,只是因为sorting后的数组可以更快地被处理。 对数组进行sorting的成本很可能比存储大得多。
使用&
或&&
仍然需要一个条件进行评估,所以不太可能会节省任何处理时间 – 它甚至可能会增加它,因为当您只需要评估一个expression式时,您正在评估这两个expression式。
如果在一些非常罕见的情况下使用&
over &&
来节省纳秒,那么您已经浪费了更多的时间来考虑与使用&
over &&
保存的差异。
编辑
我很好奇,并决定运行一些基准。
我做了这个类:
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运行了一些性能testing。 我没有使用任何打印语句来节省处理时间,只是知道这两个评估为true
。
第一个testing:
第二个testing:
第三个testing:
正如你所看到的分析testing,只用一个“ &
实际上比使用两个“ &&
要花2-3倍的时间。 这确实有些奇怪,因为我希望只有一个更好的performance。
我不是100%确定为什么。 在这两种情况下,两个expression式都必须被评估,因为两者都是正确的。 我怀疑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"); } }
这里是性能testing:
testing1:
testing2:
testing3:
This takes into account different values and different conditions as well.
Using one &
takes more time to run when both conditions are true, about 60% or 2 milliseconds more time. When either one or both conditions are false, then one &
runs faster, but it only runs about 0.30-0.50 milliseconds faster. So &
will run faster than &&
in most circumstances, but the performance difference is still negligible.
What you are after is something like this:
x <= value & value <= y value - x >= 0 & y - value >= 0 ((value - x) | (y - value)) >= 0 // integer bit-or
Interesting, one would almost like to look at the byte code. But hard to say. I wish this were a C question.
I was curious to the answer as well, so I wrote the following (simple) test for this:
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)); }
With the end result being that the comparison with && always wins in terms of speed, being about 1.5/2 milliseconds quicker than &.
EDIT: As @SvetlinZarev pointed out, I was also measuring the time it took Random to get an integer. Changed it to use a pre-filled array of random numbers, which caused the duration of the single operand test to wildly fluctuate; the differences between several runs were up to 6-7ms.
The way this was explained to me, is that && will return false if the first check in a series is false, while & checks all items in a series regardless of how many are false. IE
if (x>0 && x <=10 && x
Will run faster than
if (x>0 & x <=10 & x
If x is greater than 10, because single ampersands will continue to check the rest of the conditions whereas double ampersands will break after the first non-true condition.