流水线中的分支预测

简介:

在现代CPU中,为了提高执行的性能,CPU的多个单元会同时执行多条指令。例如当取址单元正在寻找下一条指令前,上一条指令的译码和执行已经在进行中了,这一套机制被称作CPU流水线(pipeline)。

CPU流水线架构把指令的执行分为了多个阶段,每个单元只负责完成指令执行过程中的一个阶段,而中间结果由专门的流水线寄存器暂存。这样理论上,一条指令的执行假设被分为5个阶段,那么当5个单元同时运行一段时间后,理论上相同时间可以同时执行5条指令,当然这只是最简单的情况,实际的情况要复杂得多。

400px-5_Stage_Pipeline.svg

流水线的引入相当于程序中引入了并发,相应的,会带来很多额外的问题。例如为了更好地让指令流水般地执行,不涉及顺序一致性的指令会被重排序。这里不详细讨论太多流水线的技术细节,只要知道指令并不是一条一条顺序执行的,那样会严重阻碍处理器的性能。

CPU流水线引入的目的在于,希望能够在每个CPU的时钟周期都发射一条新的指令,这样理论上可以达到最高效率。但这有一个前提:如果指令的执行是每个时钟周期一条,那么指令的取值也必须达到每个时钟周期一条,如此,当你在取址阶段拿到要执行的指令时,下一条指令的地址必须被确定了,否则下一个时钟周期便无法取出对应的指令。

这将引发一个问题:

对于条件跳转指令(汇编层面是JMP等,代码层面例如if),需要等当前的指令执行完才知道结果是true还是false,也就是说,要等待若干个时钟周期后,CPU才可以确定下一条要执行的指令究竟是哪个分支的。

难道这段时间就只能干等着吗?当然不是,这里CPU就会采取「分支预测」的方式,预测下一条要执行的分支指令,并预先执行,如果if执行完后发现和预测的分支一致,那就中大奖了,整个执行阶段一点都没有暂停。但如果悲剧地预测错误,那么这时候必须从取址开始,重新执行另一个分支的指令。

从中显然可以看出,预测错误消耗的代价要比干等着更大。

为了提高CPU预测的正确性,有多种算法,但是我们可以先思考一下最简单的方案。

首先,从常理上来考虑,如果遇到一个if分支,那么很有可能某一个分支的执行概率要大于另一个。这跟我们的编码风格有关,例如在校验参数是否合法的过程中,有的人喜欢嵌套if一直到满足一个最严格的条件,然后执行,这种情况对应了if为true的情况;另一种人喜欢先把其它条件都排除掉,例如null判断等,等不合法的都被排除后,这时候再执行,这种情况对应if为false的情况。

分支预测算法也类似,如果一个程序在80%的时间里总是选择了true的分支,那么下一次预测就更偏向于true的那个分支,分支预测的关键在于找到一种下一次分支选择的「模式」。事实上,有人试验过一种最简单的分支预测算法:总是选择true的分支,这种情况下分支预测的正确率在60%左右。

那么分支预测对于我们写代码有什么启示吗?这里让我想到了数据的局部性原理,也就是说刚刚被使用的数据,有可能被马上再次使用,回想一下,LRU Cache算法也是基于这个前提,分支预测的思想也差不多。

我们不妨来看一个简单的程序:


import java.util.Arrays;
import java.util.Random;

public class BranchPrediction {
        public static void main(String[] args) {
                Random random = new Random();
                int[] array = new int[1024 * 1024 * 20];
                for (int i = 0; i < array.length; i++) {
                        array[i] = random.nextInt();
                }
                sum(array);// warm up

                long start = System.nanoTime();
                sum(array);// Not Sorted
                System.out.println(System.nanoTime() - start);

                Arrays.sort(array);
                start = System.nanoTime();
                sum(array);// Sorted
                System.out.println(System.nanoTime() - start);
        }

        private static long sum(int[] array) {
                long sum = 0L;
                for (int i = 0; i < array.length; i++) {
                        if (array[i] < 128) {
                                sum += array[i];
                        }
                }
                return sum;
        }
}


代码的意图很简单,对一个随机数构成的数组,求小于128的数字之和。

那么数组排序与否,对累加的性能有什么影响吗?这段代码在我的电脑上执行的结果是:

未排序的数组:85612910ns

排序过的数组:20390006ns

性能差了4倍多,原因经过上面的论述也很明确了,关键在于这一句:


if (array[i] < 128)


当数组没有被排序时,这个if会随机地导向不同的分支,可以想见,CPU的分支预测将会连连受挫。

而数组被排序后,数组前半部分都走到true的分支,后半部分都走到false的分支,分支预测的效果将会相当好。

类似的CPU优化还有很多,例如cacheline、多级cache等等,有时候你要去利用它们,有时候反而要避免它们的「自作聪明」带来的成本,以后再来写写吧。

目录
相关文章
|
8月前
|
jenkins Devops 持续交付
【devops】九、Jenkins流水线(上)
【devops】九、Jenkins流水线(上)
136 1
|
11天前
|
Java jenkins 测试技术
云效流水线 Flow
云效流水线Flow是阿里云提供的企业级CI/CD工具,简化软件开发流程,提高协作效率。本报告评估了其易用性、功能、性能、开放性和成本。Flow界面直观,提供预设模板,但学习曲线略陡。功能完备,支持全生命周期管理,智能诊断功能强大。性能上,依托阿里云,具备高可用性和弹性。然而,开放性和与其他云服务的集成有待增强。成本方面,免费额度适合小项目,大项目需考虑额外费用。一个中型Java项目案例显示,Flow快速构建CI/CD流程,智能诊断节省调试时间,但在非阿里云环境集成存在挑战。
484 2
云效流水线 Flow
|
1月前
|
弹性计算 运维 Devops
云效产品使用报错问题之想用流水线A执行结束,触发流水线B,配置失败如何解决
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
|
1月前
|
jenkins 持续交付
Jenkins构建简单流水线
Jenkins构建简单流水线
22 0
|
1月前
|
Serverless
云效流水线部署函数计算任务时出现了错误
【1月更文挑战第18天】【1月更文挑战第90篇】云效流水线部署函数计算任务时出现了错误
96 1
|
1月前
|
测试技术 Serverless 持续交付
通过云效流水线进行部署任务
通过云效流水线进行部署任务
149 1
|
9月前
|
弹性计算 JavaScript Devops
云效持续交付流水线
云效持续交付流水线
283 0
|
12月前
在云效中,重新运行流水线
在云效中,重新运行流水线
83 2
|
SQL Kubernetes Java
使用流水线插件实现持续集成、持续部署
本文将介绍使用流水线插件部署 RuoYi SpringBoot 项目,并实现提交代码后自动构建、自动部署。
|
jenkins Java 测试技术
基于 Rainbond 的 Pipeline(流水线)插件
Rainbond 本身具有基于源码构建组件的能力,可以将多种编程语言的代码编译成 Docker 镜像,但是在持续集成的过程中,往往会需要对提交的代码进行静态检查、构建打包以及单元测试。之前由于 Rainbond 并没有 Pipeline 这种可编排的机制,所以用户往往只能通过集成外部的 CI ,如 Jenkins、Gitlab CI 等。这给开发者的使用增加了门槛。

热门文章

最新文章