最近思考一个问题。我们知道,在底层汇编代码中,除以2的指令效率远低于直接右移1位。所以我看到的不止一个java教学视频(原谅我看了很多民间流传的教学视频,简单粗暴)说过/2尽量写成>>1。但是另一方面,我记得上课学过编译器的优化问题,很多事情其实是不需要程序员考虑的。那么事实是怎么样的呢?

这就要考虑到java编译的流程了:.java文件先转换成.class文件(字节码),在运行的时候,JVM先接收到字节码,再做JIT即时优化和编译,形成对应的机器码。

首先我想到的是,先查看一下.class文件的字节码,看看是否进行了优化:

1
javap -verbose 文件名 //这条指令可以反编译.calss文件,查看到具体指令。

最终看到的结果,除法被编译成了idiv,右移被编译成了ishr。就是说并没有优化。(如果是常数运算会直接在编译字节码时常量折叠,此处就不考虑了)

代码的测试结果也印证了这一点:

1
2
3
4
5
6
long a1=System.currentTimeMillis();
for (long i = 0; i < 999999999l; i++) {
j=i>>1;//或j=i/2;
}
long a2=System.currentTimeMillis();
System.out.println(a2-a1);

运算结果:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int low = fromIndex;
int high = toIndex - 1;

while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];

if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.

这里用(low + high) >>> 1代替(low + high) /2是非常正确的,首先是因为数组下标肯定不会是负数,另一方面如果low + high大于int最大值(溢出变为负数了)时,只有>>>1能保证结果正确。