今天看到的一个有关cpu的问题,所以分享总结一下:
抛出一个问题
首先有一段代码
#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;
}
这段代码是将数组排序后,再遍历数组,大于128的进行累加,并记录所消耗的时间。
当我调用sort的时候输出的消耗时间为:
然而当我将sort注释掉以后再运行的消耗时间出乎预料:
不使用sort反而耗时多了3倍,这个从算法的角度是绝对解释不通的。
后来我了解到了cpu当中的分支预测,不得不感叹cpu设计的精妙。我没有学过计算机体系结构,所以对cpu的架构不太了解,只是说对cpu流水线这些有依稀的印象,所以下面的解释会非常的浅显,希望将来有机会可以学习一下计算机体系结构。
分支预测的概念
以下是概念:
分支预测器(英语:Branch predictor)是一种数字电路,在分支指令执行结束之前猜测哪一路分支将会被运行,以提高处理器的指令流水线的性能。使用分支预测器的目的,在于改善指令管线化的流程。现代使用指令管线化处理器的性能能够提高,分支预测器对于现今的指令流水线微处理器获得高性能是非常关键的技术。
然后是我的大白话:
首先分析分支预测这个概念产生的原因。首先我们知道,cpu的运行是以流水线的方式进行的,程序运行的时候会有一条非常长的流水线。当遇到一个逻辑分支时,需要依赖给出的逻辑来判断接下来的执行时。如果进行等待,则会消耗非常长的时间,会产生所谓的流水线停顿(stalled)或流水线冒泡(bubbling)或流水线打嗝(hiccup),因此分支预测就出现了。
图中一个气泡在编号为3的始终频率中产生,指令运行被延迟。
分支预测器
分支预测器是一种数字电路,在分支指令执行前,猜测哪一个分支会被执行,能显著提高pipelines的性能。
条件分支通常有两路后续执行分支,not token时,跳过接下来的JMP指令,继续执行, token时,执行JMP指令,跳转到另一块程序内存去执行。
加入分支预测器后,为避免pipeline停顿(stream stalled),其会猜测两路分支哪一路最有可能执行,然后投机执行,如果猜错,则流水线中投机执行中间结果全部抛弃,重新获取正确分支路线上的指令执行。可见,错误的预测会导致程序执行的延迟。
回到最初那个问题
会产生分支预测的代码为:
if (data[c] >= 128)
sum += data[c];
当我们对数组进行排序后,前面的元素进入if语句的时候都为false,而后面的元素进入if语句的时候都为true,这种分支的都是朝一个方向运行的方式对分支预测来说是十分友好的,因为分支预测会根据历史记录来判断我应该执行哪个分支,而之前的分支走向都是同一方向,这样使后续的分支预测都会正确。
T = 分支命中
N = 分支没有命中
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 (非常容易预测)
而当我们使用无须数组时,if语句的走向变得完全随机,分支预测的结果将变得无法预测,错误率达到了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 ... (完全随机--无法预测)
每一次分支预测的失败都导致了预测后的执行的中间结果全部抛弃,从分支处重新来过,导致的超时。
解决方法
利用位运算来代替if语句
位运算的知识:
|x| >> 31 = 0 # 非负数右移31为一定为0
~(|x| >> 31) = -1 # 0取反为-1
-|x| >> 31 = -1 # 负数右移31为一定为0xffff = -1
~(-|x| >> 31) = 0 # -1取反为0
-1 = 0xffff
-1 & x = x # 以-1为mask和任何数求与,值不变
所以可以通过以下代码替代上面的if语句
int t = (data[c] - 128) >> 31; # statement 1
sum += ~t & data[c]; # statement 2
或者不用移位运算
int t=-((data[c]>=128)); # generate the mask
sum += ~t & data[c]; # bitwise AND
结论
- 排序会影响分支预测。
- 可以使用位运算来替代if语句。
自己对于计算机底层的原理还需要加强