有如下代码:
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
int arraySize = 32768;
int data[] = new int[arraySize];
Random rnd = new Random(0);
for (int c = 0; c < arraySize; ++c)
data[c] = rnd.nextInt() % 256;
// !!! With this, the next loop runs faster
<strong>Arrays.sort(data);</strong>
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
<span style="color:#ff0000;"> </span><strong style="color: rgb(255, 0, 0);"> if (data[c] >= 128)</strong><span style="color:#ff0000;">
</span><strong><span style="color:#ff0000;"> </span>sum += data[c];</strong>
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
}
排序后的数组,执行效率与未排序时有明显区别,我本地执行时,前者是5s,后者为11s。
这是一个典型的“分支预测”问题。
假设你是18世纪的扳道工,这时远程通信技术并不发达。你并不知道每辆火车的方向,所以每当火车遇到岔道口,需要先停下来告知驶向的方向并进行调整,然后才可以启动。笨重的火车具有很大的惯性,所以需要频繁的停下启动。
有什么好的方法?你来预测火车前进的方向!
1.预测成功,火车直接前进即可。
2.预测失败,停下、换道、重启。
如果你每次都预测成功了,那么火车中途就不需要停顿了。
然而你经常预测失败,所以大量的时间被耗费在刹车、重启上。。
在处理器级别,if是典型的分支预测指令。
当程序在处理器中执行时,只有走到那一步才知道判断的结果,现代的处理器结构复杂,所以经常需要刹车、重启。
有更好的方法么?处理器来预测分支的走向!
1.预测成功,继续执行。
2.预测失败,回滚、重新执行。
如果处理器每次都预测成功,那么处理器不会回滚。
然而处理器经常预测失败,所以耗费了大量的时间在回滚上。
这就是所谓的“分支预测”,可能这个比喻并不是十分的贴切——火车可以在车头高高的插一面旗帜表示方向,但是在处理器中,直到最后一刻处理器才会知道执行的方向。
那处理器如何才能最少化回滚的次数呢?——以史为镜。如果前面99次火车都是朝左开,那么第一百次也会选择左边;如果是轮流的,那么下一次跟前一次就是相反的;如果每三次改变方向,同上。。。
简而言之,处理器试图总结一个模型,并遵循这个模型。
大部分的应用程序运行时都很有规律,领先的分支预测模型甚至能够达到90%以上的命中率,但是遇到难以预测的随机情况,分支预测机制也无能为力。
回头看上面的程序,可知造成效率差异的罪魁祸首是那个if判断,在预先没有对数组进行排序时,分支预测失效,导致处理器频繁的rollback;而在排序后,处理器很容易就找到了适用的预测模型,大大提高了效率。
解决方案:
1.在if语句之前加上排序。
2.避免分支预测,即使用位运算来替代if语句(牺牲可读性):
if (data[c] >= 128)
sum += data[c];
改为
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
即可。注意:如上代码并不适用所有环境。
验证结果稳定在5.8s。
扩展阅读:"Branch predictor" article on Wikipedia