理解处理器的“分支预测逻辑”,优化程序性能
一、什么是“分支预测逻辑”?为什么需要它?
由于处理器通过流水线技术来提高性能,而流水线要求事先知道接下来要执行的具体指令,才能保持流水线中充满待执行的指令。当在程序中遇到分支语句/条件跳转时,问题就出现了,处理器不确定下一条指令是什么,这时就需要进行“分支预测逻辑”来确定哪一条指令进入流水线。
如果预测了一个分支加入流水线,之后确发现它是错误的分支,处理器要回退该错误预测执行的工作,再用正确的指令填充流水线。这样一个错误的预测会严重浪费时钟周期,导致程序性能下降。
举个例子:一个人走到一个岔路口(分支语句),他不知道正确的路是左边还是右边。如果他选择了一条路走了一会后发现不对,他就要再返回到岔路口再走另外一条路,白白耗费了时间。如果他能一次就找到争取的路就好了(分支预测逻辑的必要性)。如何能做到呢?如果他要多次走过岔路口,发现总是右边的路是对的,这时候当再遇到岔路口时,他就可以预测右边的路是正确的(分支预测逻辑的一种原理)。
二、怎样根据“分支预测逻辑”来优化程序性能?
1.一个排序后提升程序性能的例子:
1 #include <algorithm>
2 #include <ctime>
3 #include <iostream>
4
5 int main()
6 {
7 // 产生随机数数组,随机数在0-255之间
8 const unsigned arraySize = 32768;
9 int data[arraySize];
10
11 for (unsigned c = 0; c < arraySize; ++c)
12 data[c] = std::rand() % 256;
13
14 // 关键:加上这句后程序运行明显会更快!
15 //std::sort(data, data + arraySize);
16
17 // 测试
18 clock_t start = clock();
19 long long sum = 0;
20
21 for (unsigned i = 0; i < 100000; ++i)
22 {
23 for (unsigned c = 0; c < arraySize; ++c)
24 {
25 if (data[c] >= 128) //条件分支
26 sum += data[c];
27 }
28 }
29
30 double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
31
32 std::cout << elapsedTime << std::endl;
33 std::cout << "sum = " << sum << std::endl;
34 }
25行是个岔路口,如果随机数是乱序的,分支预测的结果只有50%的正确率,严重影响性能。
然而去掉15行的注释后再执行,由于数组有序,前一部分循环在分支处总是选择同样的分支(<128),这样每到分支处就很容易预测成功,预测成功的概率在90%以上。程序执行速度加快!
还有一种利用位运算避免分支预测的巧妙方法:
把25、26行替换为:
25 int t = (data[c] - 128) >> (sizeof(int) - 1);
26 sum += (~t & data[c]);
2.编译器在将代码编译成汇编语言时,处理分支语句时,遇到条件判断的表达时很容易计算的情况,会用条件数据传送指令代替条件控制转移指令,使得无需预测下一条指令,从而改进代码效率。