读者须知:《强者恒强:x86高性能编程笺注》是云杉网络推出的系列技术分享,该系列文章将分享x86高性能开发方面的实践和思考。主要内容目录如下,欢迎各位业界同仁与我们讨论交流相关话题。
我不知道诸位怎么看,反正我对着这幅图理解起来总是有困难。提供一个简单的理解:将每条指令都想象为一个待加工的产品,在一条有5个加工工序的流水线上鱼贯而入。这样可以让CPU的每一道工序始终保持工作量饱和,也就从根本上提升了指令的吞吐和程序的性能。
a = a ^ b;
b = a ^ b;
a = b ^ a;
如果简单地将每一行代码抽象为一个XOR指令,按上图i486流水线的示意,第一条指令进入流水线Fetch阶段,然后进入D1阶段,此时第二条指令进入Fetch。在下一个机器周期,第一条指令进入D2,第二条进入D1,同时Fetch第三条指令。到此为止一切正常,但下一个机器周期,当第一条指令进入Execute阶段的时候,第二条指令并不能继续进入下一阶段,因为它所需要的变量a的最终结果,必须在第一条指令执行完毕之后才能获得。所以第二条指令会阻塞在流水线之上,等第一条指令执行完毕才会继续。而在第二条指令执行的过程中,第三条指令也会有类似的遭遇。当出现了流水线阻塞的情况,指令的流水线式执行就会与单独执行之间拉开距离,这被称为流水线“气泡”(bubble)。
除了上面的情况,还有一种常见的原因导致气泡的产生。执行每条指令所需要消耗的时间(指令周期)是不同的。当一条简单指令前面是一条耗时较长的复杂指令的时候,简单指令不得不等待复杂指令。另外,如果程序里出现
- 时钟周期:也叫震荡周期。是时钟频率(主频)的倒数,是最小的时间周期
- 机器周期:流水线中的每个阶段称为一个基本操作,完成一个基本操作所需要的时间为机器周期
- 指令周期:执行一条指令所需要的时间,一般由多个机器周期组成
if这类分支呢?这些情况都会导致流水线不能满负荷工作,从而导致性能的相对下降。
在面对问题的时候,人总是会倾向于引入一个更复杂的机制来解决问题,多级流水线就是一个例子。复杂可以反映出技术的改良,但“复杂”本身就是一个新的问题。这也许就是矛盾永远不会消失,技术也不会停止进步的原因。但“为学日益,为道日损”,愈发复杂的机制总会在某个时机之下发生大破大立,但可能现在时机还没有到来。面对“气泡”问题,处理器又引入了一个更复杂的解决方案——1995年Intel发布Pentium Pro处理器时,加入了乱序执行核心(Out-of-order core, OOO core)。
上图就是引入乱序处理核心之后的指令μ-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测试工作组副主席。
您还可以通过以下方式了解更多云杉网络的信息

关注云杉网络公众号 yunshannetworks,回复“精选”查看;

云杉网络官方网站:yunshan.net
]]>
云杉网络
September 6, 2017
技术干货