0

0

优化x86流水线层面的性能方法

WBOY

WBOY

发布时间:2024-01-03 18:32:26

|

1361人浏览过

|

来源于Linux就该这么学

转载

导读 如何在面对分支的时候选取正确的路?如果指令选取错误,整条流水线需要首先等待剩余指令执行完毕,清空之后再重新从正确的位置开始。流水线的层次越深,造成的伤害越大。
前言

性能优化,关键在于伺候好 cpu。作为一个追求性能极致的程序员,了解 cpu 的内部机制是一个不可回避的话题。这是一个需要日积月累的持续的过程,但也并不需要深入到数字电路的程度,就像一个设计 cpu 的专家并不一定精通软件设计一样,你也并不需要成为一个 cpu 专家才能写出高性能的软件。

作为一小撮人类精英送给普罗大众的珍贵礼物,能在市场上随意购买到的 CPU 其实和买不到的核武器一样代表了人类最尖端的科技水平。即便是一位 x86 CPU 专家也只能无一遗漏地讲清楚他所专攻的那一部分内容。对于我们来说,虽然不可能尽懂,但有三个部分的内容十分关键:流水线、缓存和指令集。这三个部分之中,“流水线”可以作为一条贯穿的线索。因此,承接上一篇文章中的示例,我们先来了解一下流水线。
基本概念

PU 的主要工作是依据指令执行对数据的操作。这句话基本上解释了什么是流水线。我知道能点开这篇文章的人都不可能对“流水线”这个概念一无所知,我也不想一上来就铺陈大段大段教科书式的文本,罗列各个概念的定义,这完全是在一心一意地舍本逐末。技术的发展只是事物矛盾的一种运动形式,这次我们将尝试从 CPU 的历史沿革的角度切入对流水线各个组件的介绍。

从 40 年前 Intel 生产第一颗 8086 处理器直到今天,CPU 的变化已经让你觉得以前的处理器都只能叫做“单片机”。但即便真的是淘宝上几毛钱一个的单片机,也有和今天的 i7 处理器相通的地方。8086 处理器有 14 个今天仍在使用的寄存器:4 个通用寄存器 (General Purpose Register),4 个段寄存器 (Segment Register),4 个索引寄存器 (Index Register),1 个标志位寄存器 (EFLAGS Register) 用于标示 CPU 状态,以及最后一个,指令指针寄存器 (Instruction Pointer Register),用来保存下一个需要执行的指令的地址。这个指令指针寄存器,就直接涉及到流水线的操作过程,它的持续存在,也表明了流水线基本原理的时间一致性。

从 40 年前到现在,所有 CPU 执行过的指令都遵循以下的流程:CPU 首先依据指令指针取得 (Fetch) 将要执行的指令在代码段的地址,接下来解码 (Decode) 地址上的指令。解码之后,会进入真正的执行 (Execute) 阶段,之后会是“写回”(Write Back) 阶段,将处理的最终结果写回内存或寄存器中,并更新指令指针寄存器指向下一条指令。这基本上是一个完全符合人类逻辑的设计方案。

最初,也是最自然地,CPU 会一个接一个地处理全部指令。每一个指令都按上面的过程执行完毕,然后执行下一个指令。那个时候的主要矛盾还是软件日益增长的性能需求同落后的 CPU 处理速度之间的矛盾。在摩尔定律的正确指导下,CPU 建设工作取得了历史性成果,主要矛盾发生了转移:CPU 的执行速度慢慢快过了内存读写的速度。所以每次都去内存读取指令越来越成为不能承受之重,因此在 1982 年,处理器中引入了指令缓存。

当 CPU 的速度越来越快,数据缓存作为矛盾双方互相妥协的产物也引入到处理器之中。但这些都不是治本之法。矛盾的主要方面在于,CPU 并没有以饱和的状态运转。于是在 1989 年,i486 处理器建设性地引入了五级流水线。其思路就是以拉动内需的方式消化 CPU 的过剩产能:改一次只能处理一条指令为一次处理五条。

从x86流水线层面,如何进行性能优化

从x86流水线层面,谈谈如何进行性能优化

我不知道诸位怎么看,反正我对着这幅图理解起来总是有困难。提供一个简单的理解:将每条指令都想象为一个待加工的产品,在一条有 5 个加工工序的流水线上鱼贯而入。这样可以让 CPU 的每一道工序始终保持工作量饱和,也就从根本上提升了指令的吞吐和程序的性能。
流水线引入的问题

如果简单地将每一行代码抽象为一个XOR指令,按上图 i486 流水线的示意,第一条指令进入流水线 Fetch 阶段,然后进入 D1 阶段,此时第二条指令进入 Fetch。在下一个机器周期,第一条指令进入 D2,第二条进入 D1,同时 Fetch 第三条指令。到此为止一切正常,但下一个机器周期,当第一条指令进入 Execute 阶段的时候,第二条指令并不能继续进入下一阶段,因为它所需要的变量a的最终结果,必须在第一条指令执行完毕之后才能获得。所以第二条指令会阻塞在流水线之上,等第一条指令执行完毕才会继续。而在第二条指令执行的过程中,第三条指令也会有类似的遭遇。当出现了流水线阻塞的情况,指令的流水线式执行就会与单独执行之间拉开距离,这被称为流水线“气泡”(bubble)。

时钟周期:也叫震荡周期。是时钟频率(主频)的倒数,是最小的时间周期
机器周期:流水线中的每个阶段称为一个基本操作,完成一个基本操作所需要的时间为机器周期
指令周期:执行一条指令所需要的时间,一般由多个机器周期组成

除了上面的情况,还有一种常见的原因导致气泡的产生。执行每条指令所需要消耗的时间(指令周期)是不同的。当一条简单指令前面是一条耗时较长的复杂指令的时候,简单指令不得不等待复杂指令。另外,如果程序里出现if这类分支呢?这些情况都会导致流水线不能满负荷工作,从而导致性能的相对下降。

在面对问题的时候,人总是会倾向于引入一个更复杂的机制来解决问题,多级流水线就是一个例子。复杂可以反映出技术的改良,但“复杂”本身就是一个新的问题。这也许就是矛盾永远不会消失,技术也不会停止进步的原因。但“为学日益,为道日损”,愈发复杂的机制总会在某个时机之下发生大破大立,但可能现在时机还没有到来。面对“气泡”问题,处理器又引入了一个更复杂的解决方案——1995 年 Intel 发布 Pentium Pro 处理器时,加入了乱序执行核心 (Out-of-order core, OOO core)。

乐尚团购
乐尚团购

乐尚团购系统,是一项基于PHP+MYSQL为核心开发的一套免费 + 开源专业团购系统。软件具执行效率高、模板自由切换、后台管理功能方便等诸多优秀特点。本软件是基于Web应用的B/S架构的团购网站建设解决方案的建站系统。它可以让用户高效、快速、低成本的构建个性化、专业化、强大功能的团购网站。从技术层面来看,本程序采用目前软件开发IT业界较为流行的PHP和MYSQL数据库开发技术,基于面向对象的编程,

下载
乱序执行核心(OOO core)

其实乱序执行的思想很简单:当下一条指令被阻塞的时候,从后面的指令里再找一条能执行的就好了嘛。但要完成这个工作却相当复杂。首先要保证程序的最终结果与顺序执行一致,同时要识别各类数据依赖。要达到理想的效果,除了并行执行之外,还需要对指令的粒度进一步细化,以达到以无厚入有间的效果,这样就引入了“微操作”(micro-operations, μ-ops) 的概念。在流水线的 Decode 阶段,汇编指令又被进一步拆解,最终的产物就是一系列的微操作。

引入乱序处理核心之后的指令μ-ops 处理流程。不同颜色的模块对应第一张图中不同颜色的流水线处理阶段。

Fetch 阶段没有太多变化,在 Decode 阶段,可以并行对四条指令解码,解码的最终产物就是上面提到的μ-ops。后面的 Register Alias Table 和 Reorder Buffer 可以当做是乱序执行核心的预处理阶段。

对于并行执行的微操作,或者乱序执行的操作,很有可能会同时读写同一个寄存器。所以在处理器内部,原始的寄存器便被“别名”(aliased) 为内部对软件工程师不可见的寄存器,这样原本在同一个寄存器上执行的操作便可以在临时性的不同的寄存器上执行,无论读写,互不干扰 (注意:这里要求两个操作没有数据依赖)。而对应的微操作的操作数也变为了临时性的别名寄存器,相当于一种空间换时间的策略,并且同时对微指令进行了一次基于别名寄存器的转译。

之后微操作进入 Reorder Buffer。至此,微指令已经准备就绪。它们会被放入 Reservation Station(RS) 并被并行执行。从图中可以看到相当多的执行单元 (Port X)。每一个执行单元都执行一个特定的任务,比如读取 (Load),写入 (Store),整数计算(ALU, SEE)等等。而每一条相关的微指令都可以在它所需要的数据准备好之后执行。这样耗时较长的指令和有数据依赖关系的指令,虽然单从其自身的角度看,并没有任何变化,但它们所带来的阻塞的开销,被后续指令的并行及乱序(提前)执行所分摊,化整为零,带来整体吞吐的提升。

乱序执行核心的神奇之处就在于,它能够最大限度地提升这套机制的效率,并且在外界看来,指令是在顺序执行。这里面的详细细节不在本文的讨论范畴。但乱序执行核心是如此成功,以至于引入该机制的 CPU 即便是在大工作负载的情况下乱序执行核心仍会在大部分时间处于空闲的状态,远未饱和。因此,又引入了另外一个前端 (Front-end, 包括 Fetch 和 Decode) 给该核心输送μ-ops,在系统看来,便可以抽象为两个处理核心,这也就是超线程 (Hyper-thread)N 个物理核心,2N 个逻辑核心的由来。

乱序执行也并不一定 100% 达到顺序执行代码的效果。有些时候确实需要程序员引入内存屏障来确保执行的先后顺序。

但复杂的事物总会引入新的问题,这次矛盾转移到了 Fetch 阶段。如何在面对分支的时候选取正确的路?如果指令选取错误,整条流水线需要首先等待剩余指令执行完毕,清空之后再重新从正确的位置开始。流水线的层次越深,造成的伤害越大。后续的文章,将会介绍一些在编程层面优化的方法。
作者介绍

张攀,云杉网络工程师,专注于 x86 网络软件的开发与性能优化,深度参与 ONF/OPNFV/ONOS 等组织及社区,曾任 ONF 测试工作组副主席。

相关专题

更多
if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

747

2023.08.22

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

481

2023.08.10

Java 并发编程高级实践
Java 并发编程高级实践

本专题深入讲解 Java 在高并发开发中的核心技术,涵盖线程模型、Thread 与 Runnable、Lock 与 synchronized、原子类、并发容器、线程池(Executor 框架)、阻塞队列、并发工具类(CountDownLatch、Semaphore)、以及高并发系统设计中的关键策略。通过实战案例帮助学习者全面掌握构建高性能并发应用的工程能力。

61

2025.12.01

单片机编程软件有哪些
单片机编程软件有哪些

单片机常用的编程软件有:1、Keil编程开发环境;2、IAR;3、STC-ISP;4、STM32CubeIDE;5、Altium Designer。更多关于单片机编程软件的内容,大家可以访问本专题下面的文章。

1224

2023.10.12

单片机编程软件推荐
单片机编程软件推荐

常见的单片机编程软件可分为三类:1、专有软件,如keil uvision和iar embedded workbench;2、开源软件,如arduino ide和eclipse with cdt;3、跨平台软件,如visual studio code和atom。想了解更多单片机的相关内容,可以阅读本专题下面的文章。

511

2024.05.20

plc和单片机的区别
plc和单片机的区别

plc和单片机的区别:1、体积和结构不同:PLC体型通常较大,带有模块化结构,由多个组件组成,而单片机体型更小,通常由一个单芯片组成;2、存储容量不同:PLC通常具有较大的存储容量,用于存储程序和数据,单片机存储容量通常较小,但足以满足其应用需求;3、可编程性不同等等。想了解更多plc的相关内容,可以阅读本专题下面的文章。

613

2024.05.30

树莓派和单片机的区别
树莓派和单片机的区别

树莓派和单片机的主要区别在于功能和应用。树莓派基于linux操作系统,拥有强大的计算能力和丰富的软件生态系统,适用于物联网、小型服务器、教育和爱好者项目等场景。单片机基于简单的微控制器,计算能力有限,主要用于嵌入式系统和控制特定设备,如工业控制、家用电器和医疗设备。想了解更多树莓派和单片机的相关内容,可以阅读本专题下面的文章。

405

2024.06.03

dsp和单片机的区别
dsp和单片机的区别

dsp专注于数字信号处理,具有更高的处理能力、专门架构、指令集和存储器结构,但功耗也更高。单片机更适合一般任务,具有较低的处理能力、更通用的架构和指令集,以及较小的存储器结构和功耗。想了解更多dsp和单片机的相关内容,可以阅读本专题下面的文章。

678

2024.06.04

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

相关下载

更多

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.4万人学习

Git 教程
Git 教程

共21课时 | 2.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号