这是我在逛 Stack Overflow 时遇见的一个高分问题:Why is processing a sorted array faster than an unsorted array?,我觉得这是一个非常好的用来讲分支预测(Branch Prediction)的例子,分享给大家看看

一、问题引入

先看这个代码:

#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 数组中的伪随机数始终不变,为接下来的对比分析做准备,尽可能减少实验中的变量

我们编译并运行这段代码(gcc 版本 4.1.2,太高的话会被优化掉):

$ g++ a.cpp -o a -O3
$ ./a
processTime: 1.78
sum: 191444000000

下面,把下面的这一行注释掉,然后再编译并运行:

std::sort(data, data + arraySize);
$ g++ a.cpp -o b -O3
$ ./b
processTime: 10.06
sum: 191444000000

注意到了吗?去掉那一行排序的代码后,整个计算时间被延长了十倍!

二、是 Cache Miss 导致的吗?

答案显然是否定的。cache miss 率并不会因为数组是否排序而改变,因为两份代码取数据的顺序是一样的,数据量大小是一样的,数据布局也是一样的,并且在同一台机器上运行,并没有任何差别,所以可以肯定的是:和 cache miss 无任何关系

为了验证我们的分析,可以用 valgrind 提供的 cachegrind tool 查看 cache miss 率:

$ 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 无关

三、Branch Prediction

使用到 valgrind 提供的 callgrind tool 可以查看分支预测失败率:

$ 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

Why is processing a sorted array faster than an unsorted array?的更多相关文章

  1. Why is processing a sorted array faster than an unsorted array(Stackoverflow)

    What is Branch Prediction? Consider a railroad junction: Image by Mecanismo, via Wikimedia Commons. ...

  2. find K maximum value from an unsorted array(implement min heap)

    Maintain a min-heap with size = k, to collect the result. //Find K minimum values from an unsorted a ...

  3. 108.Convert Sorted Array to Binary Search Tree(Array; Divide-and-Conquer, dfs)

    Given an array where elements are sorted in ascending order, convert it to a height balanced BST. 思路 ...

  4. Kth Smallest Element in Unsorted Array

    (referrence: GeeksforGeeks, Kth Largest Element in Array) This is a common algorithm problem appeari ...

  5. JavaScript,通过分析Array.prototype.push重新认识Array

    在阅读ECMAScript的文档的时候,有注意到它说,数组的push方法其实不仅限于在数组中使用,专门留作通用方法.难道是说,在一些类数组的地方也可以使用?而哪些是和数组非常相像的呢,大家或许一下子就 ...

  6. String方法,js中Array方法,ES5新增Array方法,以及jQuery中Array方法

    相关阅读:https://blog.csdn.net/u013185654/article/details/78498393 相关阅读:https://www.cnblogs.com/huangyin ...

  7. numpy array转置与两个array合并

    我们知道,用 .T 或者 .transpose() 都可以将一个矩阵进行转置. 但是一维数组转置的时候有个坑,光transpose没有用,需要指定shape参数, 在array中,当维数>=2, ...

  8. [Javascript] Different ways to create an new array/object based on existing array/object

    Array: 1. slice() const newAry = ary.slice() 2. concat const newAry = [].concat(ary) 3. spread oprea ...

  9. php xml转数组,数组转xml,array转xml,xml转array

    //数组转XML function arrayToXml($arr) { $xml = "<xml>"; foreach ($arr as $key=>$val) ...

随机推荐

  1. java开发常用jar包介绍(转载)

    jta.jar 标准JTA API必要 commons-collections.jar 集合类 必要 antlr.jar  ANother Tool for Language Recognition ...

  2. Oracle dmp文件导入(还原)到不同的表空间和不同的用户下

    ------------------------------------- 从生产环境拷贝一个dmp备份文件,在另外一台电脑上搭建测试环境,用imp命令导入dmp文件时提示如下错误: 问题描述: IM ...

  3. linxu ffmpeg 编译安装

    1.下载ffmpeg. 下载网址:http://www.ffmpeg.org/download.html 2.解压缩 tar -zxvf ffmpeg-2.0.1.tar.gz 3.配置,生成Make ...

  4. .net中对象序列化技术浅谈

    .net中对象序列化技术浅谈 2009-03-11 阅读2756评论2 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储 ...

  5. solr多core的处理

    有2中配置方式,一是从Solr Admin进行multi core的配置. 在Solr Admin控制台里面选择:Core Admin 选择Add Core 然后把你准备好的路径写到里面去. name ...

  6. 小白学数据分析----->DNU/DAU

    行业指标观察分析-DNU/DAU 写在分析之前 一直以来,我们对于数据都是在做加法,也希望这个过程中,不断搜罗和变换出来更多的数据指标,维度等等.而在实际的分析中,我们发现,一如我们给用户提供产品一样 ...

  7. Redis安装及HA(High Availability)配置

    Redis是一种内存数据库,以KEY-VALUE(即键值对)的形式存储数据.这篇文章主要介绍的是Redis安装及配置,所以不对Redis本身作详细介绍了. 下载: http://redis.io/do ...

  8. C# 汉字转拼音 使用微软的Visual Studio International Pack 类库提取汉字拼音首字母

    代码参考该文http://www.cnblogs.com/yazdao/archive/2011/06/04/2072488.html VS2015版本 1.使用Nuget 安装 "Simp ...

  9. 转:简单的RTSP消息交互过程

    简单的RTSP消息交互过程 C表示RTSP客户端,S表示RTSP服务端 1.   第一步:查询服务器端可用方法 1.C->S:OPTION request       //询问S有哪些方法可用 ...

  10. JAVA自动化测试中多数据源的切换

    在做自动化测试时,数据驱动是一个很重要的概念,当数据与脚本分离后,面对茫茫多的数据,管理数据又成了一个大问题,而数据源又可能面对多个,就跟在开发过程中,有时候要连接MYSQL,有时候又要连接SQL S ...