最近思考一个问题。我们知道,在底层汇编代码中,除以2的指令效率远低于直接右移1位。所以我看到的不止一个java教学视频(原谅我看了很多民间流传的教学视频,简单粗暴)说过/2尽量写成>>1。但是另一方面,我记得上课学过编译器的优化问题,很多事情其实是不需要程序员考虑的。那么事实是怎么样的呢?
这就要考虑到java编译的流程了:.java文件先转换成.class文件(字节码),在运行的时候,JVM先接收到字节码,再做JIT即时优化和编译,形成对应的机器码。
首先我想到的是,先查看一下.class文件的字节码,看看是否进行了优化:
1 | javap -verbose 文件名 //这条指令可以反编译.calss文件,查看到具体指令。 |
最终看到的结果,除法被编译成了idiv,右移被编译成了ishr。就是说并没有优化。(如果是常数运算会直接在编译字节码时常量折叠,此处就不考虑了)
代码的测试结果也印证了这一点:
1 | long a1=System.currentTimeMillis(); |
运算结果:
j=i/2; 1070ms
j=i>>1; 702ms
那么说到这里,为什么JVM没有把除法直接优化成右移呢,因为对于负数来说,右移不等于/2。举个例子:
-5 / 2 = -2
-5 >> 1 = -3
-5 >>> 1 = 2147483645
变量在编译期间如果再加一次正负数判断,往往是得不偿失的。因此:
对于乘法和以及%运算,JVM一定会优化,这些是不需要程序员去考虑的,直接去用*%即可。
对于除法,因为上述问题,确实是位移更快些。
最后引申一下,虽然我们要“充分相信编译器”,但有些时候右移可能是最佳选择,例如java.util.Arrays.binarySearch:
1 | int low = fromIndex; |
这里用(low + high) >>> 1代替(low + high) /2是非常正确的,首先是因为数组下标肯定不会是负数,另一方面如果low + high大于int最大值(溢出变为负数了)时,只有>>>1能保证结果正确。