聊一聊分支预测,思考为什么使用 if/else 语句会降低程序效率
写在前面
如果觉得写得好,有所收获,记得点个关注和点个赞哦,感谢支持。
在Stack Overflow上看到了这样的一个帖子,觉得挺值得学习的,这个帖子是关于讨论为什么处理排序数组比处理未排序数组快?看完后面的回答,然后得到了一个概念,就是“分支预测”,然后针对分支预测查看了许多资料和论文,觉得收获挺多的,所以写一篇博文记录一下。
引出问题
可能有很多人没有接触过“分支预测”,不要着急,我们在正式讲解分支预测之前,我们先来探讨一下上面的问题,为什么处理排序数组比处理未排序数组快?我们先来看一下下面这样一段代码
import java.util.Arrays;
import java.util.Random;public class Main{public static void main(String[] args){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;// !!! 第一次运行的时候不进行排序,即注释掉下面的这个,第二次运行的时候排序Arrays.sort(data);long start = System.nanoTime();long sum = 0;for (int i = 0; i < 100000; ++i){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);}
}
上面的这段代码我们运行两次,第一次把排序的那行注释掉,第二次进行排序,可以观察一下运行的结果,我这里运行的
左边的是未排序的,右边的是排序的,发现两种同样的遍历,排序和未排序之间的结果相差十秒之多,这是为什么?要知道,因为我们上面的代码是直接new出来的数组,所以排除数据被带入缓存的因素,当然如果你觉得会不会是编译器的因素,你大可以换编译器尝试一下,结果都是相似的,更甚者你可以使用C++或者其他语言进行尝试,其结果都是相似的结果,那么这到底是是为什么呢?其实这就涉及到计算机科学中一个非常重要的概念,就是分支预测。下面我们就来了解这个概念。
分析问题的原因
首先我们仔细看一下代码,在for循环中,我们是使用if
分支进行判断操作,现在我们来这样考虑一个if
语句,在处理器级别,它是一条分支指令。这样想,如果你是处理器,并且看到一个分支。你不知道它将走哪条路。你会怎么做?你应该停止执行当前的操作并等待之前的指令完成,然后再沿着正确的路径继续进行下一步操作。现代处理器很复杂,而且流程很长。因此,他们需要一直进行“热身”和“放慢脚步”的操作。那有没有更好的办法来解决这个问题呢?其中一个办法就是,通过猜测分支将会朝哪个方向前进,也就是进行结果预测。
- 如果猜对了,则继续执行。
- 如果猜错了,则需要刷新当前的操作缓存并回滚到分支。然后可以沿着正确的路径重新启动。
这就是分支预测,要知道,在计算机中,处理器直到最后一刻才知道分支的方向。所以进行分支预测式非常有效的一种方式,我们只要通过过去大量的分支行为来进行预测, 就可以提高预测的效率。换句话说,我们可以尝试识别一个模式并遵循这个模式,这或多或少是分支预测变量的工作方式。在现实开发中,大多数应用程序都具有行为良好的分支(这里的行为良好指的是分支的行为结果存在规律性)。因此,现代分支预测器通常将达到90%以上的命中率。但是,当面对没有可识别模式的不可预测分支时,分支预测变量实际上是没有用的。如果感兴趣想要深入的话,可以看这篇文章。到这里,我们就可以知道一件事情,上面排序和未排序之所以会有的差异的原因,是在于if
分支,也就是下面这一段代码
if (data[c] >= 128)sum += data[c];
我们可以知道,数据在0到255之间均匀分布,如果对数据进行排序时,大约前半部分的迭代将不会进入if
语句。而后半部分都会进入if
语句。这种情况就是上面所说的“规律性”,这对分支预测器非常友好,因为分支连续多次朝同一方向前进。即使是简单的饱和计数器也可以正确预测分支。我们来举个例子,假设排序之后的数据是如下这样的,我们将它可视化一下
当数据完全随机时,分支预测器将变得没有什么用,因为它无法预测随机数据。因此,可能会有大约50%的错误预测(没有比随机猜测好)同样我们可视化一下数据。
如何提高程序效率
通过上面的分析我们知道了因为分支的预测性,导致程序的效率下降的问题,我们怎么来解决这个问题呢?最直接的就是将数据进行规律性整理,保证分支预测的准确率,从而提高程序的效率。当然,如果编译器将数据进行规划性整理比较困难,我们还有一种方法,就是可以稍微牺牲一点程序的可读性来提高效率,即通过二进制位运算来解决。比如我们可以进行这样的替换
if (data[c] >= 128)sum += data[c];
将上面的这段代码换成下面这样的
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
这样做直接消除了分支,并用一些按位运算将其替换(请注意,这种解决办法只是举个例子,在本例中并不完全等同于原始的if
语句。但是在这种情况下,它对于的所有输入值均有效data[]
)。简而言之,就是分支可能会影响程序的效率,我们可以通过一些方式,避免使用分支,或者提高分支的效率(如排序),下面我把数据排序与未排序,分支和未分支的时间贴出来
// 分支+乱序
seconds = 16.93293813
// 分支+排序
seconds = 6.643797077
// 分支+乱序
seconds = 3.113581453
// 未分支+排序
seconds = 3.186068823
进一步讲讲分支预测
条件分支指令通常具有两路后续执行分支。即不采取(not taken)跳转,顺序执行后面紧挨JMP的指令,以及采取(taken)跳转到另一块程序内存去执行那里的指令。是否需要跳转,只有到真正执行时才能确定。如果没有分支预测器,处理器将会等待分支指令通过了指令流水线的执行阶段,才把下一条指令送入流水线的第一个阶段—取指令阶段(fetch stage),这种技术叫做 流水线停顿。分支预测器就是猜测条件判断会走哪一路,如果猜对,就避免流水线停顿造成的时间浪费。如果猜错,那么流水线中推测执行的那些中间结果全部放弃,重新获取正确的分支路线上的指令开始执行,这导致了程序执行的延迟。
什么是指令流水线?
开发计算机程序,本质上是编写一组期望计算机顺序执行的命令。早期的计算机一次仅执行一条命令。这意味着每个命令都会加载到内存中,执行完成后再加载下一个命令。指令流水线是一种改进。处理器会将工作分解成多个部分,对不同的部分并行执行。这样,处理器能够在加载下一条的同时执行一条命令。处理器内部的指令流水线越长,不仅可以简化还能并行执行更多的部分。这样能够提高系统的整体性能。例如下面这个简单的程序:
int a = 0;
a += 1;
a += 2;
a += 3;
程序会按照下面的流水线处理:Fetch(提取)、Decode(解码)、Execute(执行)、Store(存储):
这里可以看到四个命令如何并行处理,整体执行速度更快。处理器执行某些命令时会导致流水线问题。流水线中部分指令执行时依赖于之前的指令,但是前面的指令可能还没有执行。分支是一种危险。分支会挑选两个执行方向之一执行,但只有在解析后才能确定是哪个方向。这意味着通过分支加载命令都是不安全的,因为无法知道从哪里加载命令。修改上面的程序加入分支:
int a = 0;
a += 1;
if (a < 10) {a += 2;
}
a += 3;
运行结果与之前相同,但其中加入了 if语句。在解析前,虽然计算机能看到这些指令,但不能加载 if
后面的指令。因此,执行的顺序看起来像下面这样:
现在可以立刻看到加入分支对程序执行造成的影响,得到相同结果所需的时钟周期。分支预测是对上面的一种改进,计算机会尝试预测分支的执行路径,然后采取相应的动作。在上面的示例中,处理器会预测if(a <10)为 true
,因此把 a += 2
作为下一条待执行指令。这将导致执行的顺序变成这样:
可以看到程序的性能立即得到了提升:现在只要9个时钟周期而不是之前的11个,速度提升了19%。但是,这样做也并非没有风险。如果分支预测出错,那么将对不应该执行的指令排队。发生这种情况时,计算机要丢弃这些指令重新开始。修改判断条件改为false:
int a = 0;
a += 1;
if (a > 10) {a += 2;
}
a += 3;
可能会像下面这样执行:
现在,即使处理的指令更少,执行却比之前慢!处理器错误地预测分支等于 true,把 a += 2 指令排队。接着发现分支等于 false,必须丢弃已排队的指令,然后重新执行。到此我们就饿差不多了解了分支预测的概念,这里多提一句,如果我们不能去掉分支,我们可以保证分支的顺序,if/else
语句的分支顺序很重要。也就是说,下面这样的分支安排性能会更好:
if (mostLikely) {// Do something
} else if (lessLikely) {// Do something
} else if (leastLikely) {// Do something
}
聊一聊分支预测,思考为什么使用 if/else 语句会降低程序效率相关推荐
- 【超标量】分支预测的方向预测总结
一.静态分支预测 静态分支预测就是不预测,每次在流水线的后续阶段,得到了分支指令实际方向和地址后再进行判断,若跳转,则清空流水线.若不跳转则继续执行.故静态分支预测的成功率是50%. 静态分支预测的方 ...
- 阿里程序员工作小技巧:理解CPU分支预测,提高代码效率
技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,体现也会在优秀程序员在工作效率提升,产品性能优化和用户体验改善等小技巧方面的分享,以提高我们的工作能力 ...
- 阿里程序员工作小技巧 | 理解CPU分支预测,提高代码效率
技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,也会体现在优秀程序员在工作效率提升.产品性能优化和用户体验改善等小技巧方面的分享,以提高我们的工作能力 ...
- CPU的流水线,分支预测与乱序执行
流水线 转自:http://www.elecfans.com/emb/dsp/20180405657563.html 流水线的概念来源于工业制造领域,以汽车装配为例来解释流水线的工作方式,假设装配一辆 ...
- 【linux】Valgrind工具集详解(十四):Cachegrind(缓存和分支预测分析器)
一.概述 Cachegrind,它模拟CPU中的一级缓存I1,Dl和二级缓存,能够精确地指出程序中cache的丢失和命中.如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每 ...
- 时序图 分支_BOOM微架构学习(1)——取指单元与分支预测
之前在RISC-V的"Demo"级项目--Rocket-chip一文中曾经简介过BOOM处理器的流水线,这次我们开始一个系列,深入学习一下BOOM的微架构,这样对于乱序执行的超标量 ...
- 慌!还不了解Java中的分支预测?!
点击上方"朱小厮的博客",选择"设为星标" 后台回复"1024"获取公众号专属1024GB资料 来源:rrd.me/fLHvf 1.引言 分 ...
- Purpose of cmove instruction in x86 assembly? | cmove 指令如何避免错误的分支预测带来的开销?
cmove 指令是用来做什么的?(Purpose of cmove instruction in x86 assembly?) The purpose of cmov is to allow soft ...
- 经典问题之「分支预测」
问题 来源 :stackoverflow 为什么下面代码排序后累加比不排序快? public static void main(String[] args) {// Generate dataint ...
最新文章
- 中国林科院亚热带林业研究所林木根际微生物博士后招聘启事
- 从0到1,一步步开发React的loading组件,并发布到npm上
- Python 基本输出
- jquery-循环遍历
- android画布原理,Android触摸事件如何实现笔触画布详解
- delphi listview1添加指定列_对表格的列进行批量处理的函数详解
- web.xml filter 不包含_PHP文件包含
- 两台服务器怎么发文件,两台服务器怎么发文件
- 数据分析面试都会问到哪些问题
- 职场 | 如何说服上级?这里有三个故事
- MyBatis学习笔记三——映射配置文件
- 随记 C#读取TXT文件乱码
- 牛客题库 题解 | #[NOIP2017]图书管理员#
- 攀藤PM2.5传感器使用定义串口usb数据接收
- quartusII 9.1 USB blaster驱动安装
- 1000行代码徒手写正则表达式引擎【1】--JAVA中正则表达式的使用
- 学习 Go 语言 0x04:《Go 语言之旅》中切片的练习题代码
- Unity 灯光与渲染 (一)
- ClickHouse 数据导出导入
- matlab命令中什么时候加分号