Stackoverflow上有人提了一个关于分支预测的问题,一位大神非常专业详尽的回答了这个问题,这里翻译过来供大家围观,翻译的不好,请大家见谅。
问题:
下面这段C++代码显示了一个非常奇怪的性能问题。不知什么原因,排序的data比未排序的data要神奇的快上差不多6倍。
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster
std::sort(data, data + arraySize);
// Test
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl; std::cout << “sum = ” << sum << std::endl;
}
l 没std::sort(data, data + arraySize);有时,代码返回11.54秒。
l 数据有排序时,代码返回1.93秒
开始我认为它可能和语言或者编译器相关,所以我在Java中也试了下:
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
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0); System.out.println(“sum = ” + sum); }
}
其结果差不多。
下面是大神的回答:
首先让我们来看一下铁路枢纽:
为了方便讨论,假设我们回到了18世纪,一个没有长途电话或无线通信的年代。
假设你是一个火车搬轨道工人,现在你听到一辆火车马上要开过来了。但是你并知道这辆火车将开往哪个方向,因此你停下了这趟车,然后问司机他想去哪里。然后你把轨道调整到合适的位置。
火车非常的重,有很大的惯性,因此它的启动和停止都需要花费很大的时间。
有没有更好的办法解决这个问题呢?你可以猜猜火车将要前进的方向。
l 如果你猜对了,火车继续前进。
l 如果你猜错了,司机停车,倒车,然后你调整轨道方向。司机再启动火车开往正确的方向。
如果你每次都猜对了,火车永远不需要在这儿停。
如果你经常猜错,火车将要花费大量的时间停车,倒车,重启动。
再来看看 if语句:在处理器层面,它是一个分支指令:
现在假设你是一个处理器了,你看到一个分支,你不知道分支将会怎样走。你会怎么办?你停止执行指令,等待前一天指令执完毕。然后你继续执行正确的路径。
现代处理器都是结构复杂的并且有长管道。所以它们花费较长时间“预热”和“减速”。
有没有更好的办法?你可以猜测分支走向。
l 如果猜对了,继续执行。
l 如果猜错了,你需要清空管道,回滚分支。然后重新启动另一条路径。
如果你每次都猜对了,指令执行可以一直执行。
如果你经常猜错,你会花费大量时间来停止,回滚,重新执行。
这就是分支预测。必须承认这不是一个最好的比喻,因为火车可以通过信号来指示前进方向。但是在计算机中,处理器只有在最后一刻才知道的分支的走向。
那么如何从战略将错误减少的最少呢?你可以看看过去的历史!如果火车在99%的时候都是左转,那么你就猜测左边。如果发生了调整,那么跟着调整你的猜测。如果每过三次走向左边,你也就这样猜测….
换句话说,你尝试建立一个模式,然后跟随这个模式。这大致就是分支预测的工作原理。
大多数应用程序的分支都比较规律。所以现代处理器的分支预测基本上都能达到>90%的命中率。如果遇到了这些没有模式的不可预测分支,分支预测就无用武之地了。
关于分支预测更详细的内容请戳这里: http://en.wikipedia.org/wiki/Branch_predictor
根据以上线索,影响代码性能的关键就是下面这个if语句:
if (data[c] >= 128)
sum += data[c];
注意到data均匀的分布在0~255之间。当data被排序后,大概前一半的遍历不会进入if语句,而后将一直进入if语句。
由于分支会连续多次的进入同一个分支方向,这对于分支预测来说非常的方便有利。即便是最简单的saturating counter也会正确的预测分支,除了方向切换后的少许遍历。
演示:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, … 126, 127, 128, 129, 130, … 250, 251, 252, …branch = N N N N N … N N T T T … T T T …
= NNNNNNNNNNNN … NNNNNNNTTTTTTTTT … TTTTTTTTTT (easy to predict)
然而,当data完全随机的时候,分支预测就一点用也没有了,因为它不能预测随机数。那么它可能就会发生50%的错误预测了。(比随机猜测强不了多少)
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, …branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N …
= TTNTTTTNTNNTTTN … (completely random - hard to predict)
我们该如何做?
如果计算机不能将分支优化为条件转移指令,你可以牺牲一些代码可读性通过骇客的方法来提供程序性能。
将:
if (data[c] >= 128)
sum += data[c];
替换为:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
这些通过位操作来消除if分支语句。(需要注意的是,这种骇客方法并不严格的等同于原if分支语句,但是该例中,它对data[]所有的输入都是有效的。)
基准测试:
C++ - Visual Studio 2010 - x64 Release
// Branch - Randomseconds = 11.777
// Branch - Sortedseconds = 2.352
// Branchless - Randomseconds = 2.564
// Branchless - Sortedseconds = 2.587
Java - Netbeans 7.1.1 JDK 7 - x64
// Branch - Randomseconds = 10.93293813
// Branch - Sortedseconds = 5.643797077
// Branchless - Randomseconds = 3.113581453
// Branchless - Sortedseconds = 3.186068823
结果分析:
l 有分支:排序后的和未排序的存在巨大差异。
l 骇客方法:排序后的和未排序的没有差别。
l 在C++中:骇客的方法比排序后的带分支判断的稍微慢一点点。
根据一般经验应该避免在关键的循环中使用与数据无关的分支结构。
编译器相关:
l 在x64机器上的GCC 4.6.1 中使用-O3或者-ftree-vectorize参数可以生成条件转移指令。因此排序和未排序是没有区别的,他们都飞快的。
l VC++2010 尽管使用/Ox也不能为该分支生成条件转移指令。
l Inter Complier 11 干的非常神奇。它交换这两个循环,从而提升不可预测分支到外循环。所以它不仅仅是避免了错误预测,同时也比VC2010和GCC生成的代码快上2倍!换句话说,ICC利用test-loop来打败了基准测试。
l 如果你给ICC没有分支的代码,它仅仅是较好的向量化它…并且和带分支的一样快(使用循环交换)。
这些展示了即使最成熟的现代编译器,在代码优化能力上也是千差万别的。
原文链接:www.stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array