这是我在逛 Stack Overflow 时碰见的一个高分问题:Why is processing a sorted array faster than an unsorted array?,我以为这是一个很是好的用来说分支预测(Branch Prediction)的例子,分享给你们看看ios
先看这个代码:数组
#include <algorithm> #include <ctime> #include <iostream> #include <stdint.h> int main() { uint32_t arraySize = 20000; uint32_t data[arraySize]; for (uint32_t i = 0; i < arraySize; ++ i) { data[i] = std::rand() % 256; } // !!! With this, the next loop runs faster std::sort(data, data + arraySize); clock_t start = clock(); uint64_t sum = 0; for (uint32_t cnt = 0; cnt < 100000; ++ cnt) { for (uint32_t i = 0; i < arraySize; ++ i) { if (data[i] > 128) { sum += data[i]; } } } double processTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC; std::cout << "processTime: " << processTime << std::endl; std::cout << "sum: " << sum << std::endl; return 0; };
注意:这里特意没有加随机数种子是为了确保 data 数组中的伪随机数始终不变,为接下来的对比分析作准备,尽量减小实验中的变量bash
咱们编译并运行这段代码(gcc 版本 4.1.2,过高的话会被优化掉):ide
$ g++ a.cpp -o a -O3 $ ./a processTime: 1.78 sum: 191444000000
下面,把下面的这一行注释掉,而后再编译并运行:oop
std::sort(data, data + arraySize);
$ g++ a.cpp -o b -O3 $ ./b processTime: 10.06 sum: 191444000000
注意到了吗?去掉那一行排序的代码后,整个计算时间被延长了十倍!布局
答案显然是否认的。cache miss 率并不会由于数组是否排序而改变,由于两份代码取数据的顺序是同样的,数据量大小是同样的,数据布局也是同样的,而且在同一台机器上运行,并无任何差异,因此能够确定的是:和 cache miss 无任何关系优化
为了验证咱们的分析,能够用 valgrind 提供的 cachegrind tool 查看 cache miss 率:ui
$ valgrind --tool=cachegrind ./a ==26548== Cachegrind, a cache and branch-prediction profiler ==26548== Copyright (C) 2002-2015, and GNU GPL'd, by Nicholas Nethercote et al. ==26548== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==26548== Command: ./a ==26548== --26548-- warning: L3 cache found, using its data for the LL simulation. --26548-- warning: specified LL cache: line_size 64 assoc 20 total_size 15,728,640 --26548-- warning: simulated LL cache: line_size 64 assoc 30 total_size 15,728,640 processTime: 68.57 sum: 191444000000 ==26548== ==26548== I refs: 14,000,637,620 ==26548== I1 misses: 1,327 ==26548== LLi misses: 1,293 ==26548== I1 miss rate: 0.00% ==26548== LLi miss rate: 0.00% ==26548== ==26548== D refs: 2,001,434,596 (2,000,993,511 rd + 441,085 wr) ==26548== D1 misses: 125,115,133 ( 125,112,303 rd + 2,830 wr) ==26548== LLd misses: 7,085 ( 4,770 rd + 2,315 wr) ==26548== D1 miss rate: 6.3% ( 6.3% + 0.6% ) ==26548== LLd miss rate: 0.0% ( 0.0% + 0.5% ) ==26548== ==26548== LL refs: 125,116,460 ( 125,113,630 rd + 2,830 wr) ==26548== LL misses: 8,378 ( 6,063 rd + 2,315 wr) ==26548== LL miss rate: 0.0% ( 0.0% + 0.5% )
$ valgrind --tool=cachegrind ./b ==13898== Cachegrind, a cache and branch-prediction profiler ==13898== Copyright (C) 2002-2015, and GNU GPL'd, by Nicholas Nethercote et al. ==13898== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==13898== Command: ./b ==13898== --13898-- warning: L3 cache found, using its data for the LL simulation. --13898-- warning: specified LL cache: line_size 64 assoc 20 total_size 15,728,640 --13898-- warning: simulated LL cache: line_size 64 assoc 30 total_size 15,728,640 processTime: 76.7 sum: 191444000000 ==13898== ==13898== I refs: 13,998,930,559 ==13898== I1 misses: 1,316 ==13898== LLi misses: 1,281 ==13898== I1 miss rate: 0.00% ==13898== LLi miss rate: 0.00% ==13898== ==13898== D refs: 2,000,938,800 (2,000,663,898 rd + 274,902 wr) ==13898== D1 misses: 125,010,958 ( 125,008,167 rd + 2,791 wr) ==13898== LLd misses: 7,083 ( 4,768 rd + 2,315 wr) ==13898== D1 miss rate: 6.2% ( 6.2% + 1.0% ) ==13898== LLd miss rate: 0.0% ( 0.0% + 0.8% ) ==13898== ==13898== LL refs: 125,012,274 ( 125,009,483 rd + 2,791 wr) ==13898== LL misses: 8,364 ( 6,049 rd + 2,315 wr) ==13898== LL miss rate: 0.0% ( 0.0% + 0.8% )
对比能够发现,他们俩的 cache miss rate 和 cache miss 数几乎相同,所以确实和 cache miss 无关this
使用到 valgrind 提供的 callgrind tool 能够查看分支预测失败率:code
$ valgrind --tool=callgrind --branch-sim=yes ./a ==29373== Callgrind, a call-graph generating cache profiler ==29373== Copyright (C) 2002-2015, and GNU GPL'd, by Josef Weidendorfer et al. ==29373== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==29373== Command: ./a ==29373== ==29373== For interactive control, run 'callgrind_control -h'. processTime: 288.68 sum: 191444000000 ==29373== ==29373== Events : Ir Bc Bcm Bi Bim ==29373== Collected : 14000637633 4000864744 293254 23654 395 ==29373== ==29373== I refs: 14,000,637,633 ==29373== ==29373== Branches: 4,000,888,398 (4,000,864,744 cond + 23,654 ind) ==29373== Mispredicts: 293,649 ( 293,254 cond + 395 ind) ==29373== Mispred rate: 0.0% ( 0.0% + 1.7% )
能够看到,在计算 sum 以前对数组排序,分支预测失败率很是低,几乎至关于没有失败
$ valgrind --tool=callgrind --branch-sim=yes ./b ==23202== Callgrind, a call-graph generating cache profiler ==23202== Copyright (C) 2002-2015, and GNU GPL'd, by Josef Weidendorfer et al. ==23202== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==23202== Command: ./b ==23202== ==23202== For interactive control, run 'callgrind_control -h'. processTime: 287.12 sum: 191444000000 ==23202== ==23202== Events : Ir Bc Bcm Bi Bim ==23202== Collected : 13998930783 4000477534 1003409950 23654 395 ==23202== ==23202== I refs: 13,998,930,783 ==23202== ==23202== Branches: 4,000,501,188 (4,000,477,534 cond + 23,654 ind) ==23202== Mispredicts: 1,003,410,345 (1,003,409,950 cond + 395 ind) ==23202== Mispred rate: 25.1% ( 25.1% + 1.7% )
而这个未排序的就不一样了,分支预测失败率达到了 25%。所以能够肯定的是:两份代码在运行时 CPU 分支预测失败率不一样致使了运行时间的不一样
那么到底什么是分支预测,分支预测的策略是什么呢?这两个问题我以为 Mysticial 的回答 解释的很是好:
假设咱们如今处于 1800 年代,那会长途通讯或者无线通讯尚未出现。你是某个铁路分叉口的操做员,当你正在打盹的时候,远方传来了火车轰隆隆的声音。你知道又有一辆列车开过来了,可是你不知道它要走哪条路,所以列车不得不停下来,在得知它要去哪一个方向后,你把开关拨向正确的位置,列车缓缓启动驶向远方。
可是列车很重,自身的惯性很大,中止和启动都须要花很长很长的时间。有什么方法能让列车更快的到达目的地吗?有:你来猜想列车将驶向哪一个方向。
若是你猜中了,列车继续前进;若是没有猜中:司机发现路不对后刹车、倒车、冲你发一顿火,最后你把开关拨到另外一边,而后司机启动列车,走另外一条路。
如今让咱们来看看那条 if 语句:
if (data[i] >= 128) { sum += data[i] }
如今假设你是 CPU,当遇到这个 if 语句时,接下来该作什么:把 data[i]
累加到 sum
上面仍是什么都不作?
怎么办?难道是暂停下来,等待 if 表达式算出结果,若是是 true
就执行 sum += data[i]
,不然什么也不作?
通过几十年的发展,现代处理器异常复杂并拥有者超长的 pipeline,它须要花费很长的时间“暂停”和从新执行命令,为了加快执行速度,处理器须要猜想接下来要作什么,也就是说:你先忽略 if 表达式的结果,让它一边算去,你选择其中一个分支继续执行下去。
若是你猜对了,程序继续执行;若是猜错了,须要 flush pipeline、回滚到分支判断那、选择另外一个分支执行下去。
若是每次都猜中:程序执行过程当中永远不会出现中途暂停的状况
若是大多数都猜错了:你将消耗大量的时间在“暂停、回滚、从新执行”上面
这就是分支预测。那么 CPU 在猜想接下来要执行哪一个分支时有什么策略吗?固然是根据已有的经验啦:根据历史经验寻找一个模式
若是过去 99% 的火车都走了左边,你就猜想下次火车到来仍是会走左边;若是是左右交替着走,那么每次火车来的时候你把开关拨向另外一边就能够了;若是每三辆车走右边后会有一辆车走左边,那么你也对应的猜想并操做开关...
也就是说:从火车的行进方向历史中找到一个固有的模式,而后按照这个模式猜想下次火车将走哪一个方向。这种工做方式和处理器的分支预测器很是类似
大多数应用程序都有表现良好的分支选择(让 CPU 有迹可循)模式,所以现代分支预测器基本上都有着 90% 以上的命中率。可是当面临有着没法识别的分支选择模式时,分支预测器的命中率极度低下,毫无可用性可言,好比上面未排序的随机数组 data
关于分支预测的更多解释,感兴趣的话你们能够看看维基百科的解释:Branch predictor