深刻浅出计算机组成原理学习笔记:第二十四讲

一 、引子

过去两讲,我为你讲解了经过增长资源、停顿等待以及主动转发数据的方式,来解决结构冒险和数据冒险问题。对于结构冒险,因为限制来自于同一时钟周期不一样的指令,
要访问相同的硬件资源,解决方案是增长资源。对于数据冒险,因为限制来自于数据之间的各类依赖,咱们能够提早把数据转发到下一个指令。缓存

可是即使综合运用这三种技术,咱们仍然会遇到不得不停下整个流水线,等待前面的指令完成的状况,也就是采用流水线停顿的解决方案。好比说,上一讲里最后给你的例子,
即便咱们进行了操做数前推,由于第二条条加法指令依赖于第一条指令从内存中获取的数据,咱们仍是要插入一次NOP的操做。架构

 

 

那咱们能不能让后面没有数据依赖的指令,在前面指令停顿的时候先执行呢?并发

答案固然是能够的。毕竟,流水线停顿的时候,对应的电路闲着也是闲着。那咱们彻底能够先完成后面指令的执行阶段。性能

2、填上空闲的NOP:上菜的顺序没必要是点菜的顺序

以前我为你讲解的,不管是流水线停顿,仍是操做数前推,归根到底,只要前面指令的特定阶段尚未执行完成,后面的指令就会被“阻塞”住。线程

可是这个“阻塞”不少时候是没有必要的。由于尽管你的代码生成的指令是顺序的,可是若是后面的指令不须要依赖前面指令的执行结果,彻底能够没必要等待前面的指令运算完成。
好比说,下面这三行代码。3d

计算里面的 x ,却要等待 a 和 d 都计算完成,实在没啥必要。因此咱们彻底能够在 d 的计算等待 a 的计算的过程当中,先把 x 的结果给算出来。blog

一、在流水线里,后面的指令不依赖前面的指令,那就不用等待前面的指令执行,它彻底能够先执行。

在流水线里,后面的指令不依赖前面的指令,那就不用等待前面的指令执行,它彻底能够先执行。排序

 

 

能够看到,由于第三条指令并不依赖于前两条指令的计算结果,因此在第二条指令等待第一条指令的访存和写回阶段的时候,第三条指令就已经执行完成了。内存

二、上菜的顺序没必要是点菜的顺序

这就比如你开了一家餐馆,顾客会排队来点菜。餐馆的厨房里会有洗菜、切菜、炒菜、上菜这样的各个步骤。后厨也是按照点菜的顺序开始作菜的。可是不一样的菜须要花费的时间和工序可能都有差异。有些菜作起来特别麻烦,特别慢。好比作一道佛跳墙有好几道工序。咱们没有必要非要等先点的佛跳墙上菜了,再开始作后面的炒鸡蛋。只要有厨子空出来了,就能够先动手作前面的简单菜,先给客户端上去。资源

三、乱序执行

这样的解决方案,在计算机组成里面,被称为 乱序执行(Out-of-Order Execution,OoOE)。乱序执行,最先来自于著名的IBM 360。相信你必定据说过《人月神话》这本软件工程届的经典著做,
它讲的就是IBM360开发过程当中的“人生体会”。而IBM 360困难的开发过程,也少不了第一次引入乱序执行这个新的CPU技术。

3、CPU里的“线程池”:理解乱序执行

一、在CPU里,乱序执行的过程到底是怎样的。

那么,咱们的CPU怎样才能实现乱序执行呢?是否是像玩俄罗斯方块同样,把后面的指令,找一个前面的坑填进去就好了?事情并无这么简单。其实,从今天软件开发的维度来思考,乱
序执行好像是在指令的执行阶段,引入了一个“线程池”。咱们下面就来看一看,在CPU里,乱序执行的过程到底是怎样的。

使用乱序执行技术后,CPU里的流水线就和我以前给你看的5级流水线不太同样了。咱们一块儿来看一看下面这张图。

 

 

1.在取指令和指令译码的时候,乱序执行的CPU和其余使用流水线架构的CPU是同样的。它会一级一级顺序地进行取指令和指令译码的工做。

2.在指令译码完成以后,就不同了。CPU不会直接进行指令执行,而是进行一次指令分发,把指令发到一个叫做保留站(Reservation Stations)的地方。顾名思义,这个保留站,
就像一个火车站同样。发送到车站的指令,就像是一列列的火车。

3.这些指令不会马上执行,而要等待它们所依赖的数据,传递给它们以后才会执行。这就好像一列列的火车都要等到乘客来齐了才能出发。

4.一旦指令依赖的数据来齐了,指令就能够交到后面的功能单元(Function Unit,FU),其实就是ALU,去执行了。咱们有不少功能单元能够并行运行,可是不一样的功能单元可以支持执行的指令并不相同。就和咱们的铁轨同样,有些从上海北上,能够到北京和哈尔滨;有些是南下的,能够到广州和深圳。

5.指令执行的阶段完成以后,咱们并不能马上把结果写回到寄存器里面去,而是把结果再存放到一个叫做重排序缓冲区(Re-Order Buffer,ROB)的地方。

6.在重排序缓冲区里,咱们的CPU会按照取指令的顺序,对指令的计算结果从新排序。只有排在前面的指令都已经完成了,才会提交指令,完成整个指令的运算结果。

7.实际的指令的计算结果数据,并非直接写到内存或者高速缓存里,而是先写入存储缓冲区(StoreBuffer面,最终才会写入到高速缓存和内存里。

能够看到,在乱序执行的状况下,只有CPU内部指令的执行层面,多是“乱序”的。只要咱们能在指令的译码阶段正确地分析出指令之间的数据依赖关系,
这个“乱序”就只会在互相没有影响的指令之间发生。即使指令的执行过程当中是乱序的,咱们在最终指令的计算结果写入到寄存器和内存以前,依然会进行一次排序,以确保全部指令在外部看来仍然是有序完成的。

二、有了乱序执行,咱们从新去执行上面的3行代码。

有了乱序执行,咱们从新去执行上面的3行代码

a = b + c
d = a * e
x = y * z

里面的 d 依赖于 a 的计算结果,不会在 a 的计算完成以前执行。可是咱们的CPU并不会闲着,由于 x = y * z的指令一样会被分发到保留站里。由于 x 所依赖的 y 和 z 的数据是准备好的,
 这里的乘法运算不会等待计算 d,而会先去计算 x 的值。

若是咱们只有一个FU可以计算乘法,那么这个FU并不会由于 d 要等待 a 的计算结果,而被闲置,而是会先被拿去计算 x。

在 x 计算完成以后,d 也等来了 a 的计算结果。这个时候,咱们的FU就会去计算出 d 的结果。而后在重排序缓冲区里,把对应的计算结果的提交顺序,仍然设置成 a -> d -> x,
而计算完成的顺序是 x -> a -> d。在这整个过程当中,整个计算乘法的FU都没有闲置,这也意味着咱们的CPU的吞吐率最大化了。

三、整个乱序执行技术,就好像在指令的执行阶段提供一个“线程池”

整个乱序执行技术,就好像在指令的执行阶段提供一个“线程池”。指令再也不是顺序执行的,而是根据池里所拥有的资源,以及各个任务是否能够进行执行,进行动态调度。在执行完成以后,又从新把结果在一个队

列里面,按照指令的分发顺序从新排序。即便内部是“乱序”的,可是在外部看起来,仍然是层次分明地顺序执行。

乱序执行,极大地提升了CPU的运行效率。核心缘由是,现代CPU的运行速度比访问主内存的速度要快不少。若是彻底采用顺序执行的方式,不少时间都会浪费在前面指令等待获取内存数据的时间里。CPU不得不加入NOP操做进行空转。而现代CPU的流水线级数也已经相对比较深了,到达了14级。这也意味着,同一个时钟周期内并行执行的指令数是不少的。

而乱序执行,以及咱们后面要讲的高速缓存,弥补了CPU和内存之间的性能差别。一样,也充分利用了较深的流水行带来的并发性,使得咱们能够充分利用CPU的性能。

4、总结延伸

好了,总结一下。这一讲里,我为你介绍了乱序执行,这个解决流水线阻塞的技术方案。由于数据的依赖关系和指令前后执行的顺序问题,不少时候,流水线不得不“阻塞”在特定的指令上。即便后续别的指令,并不依赖正在执行的指令和阻塞的指令,也不能继续执行。

而乱序执行,则是在指令执行的阶段经过一个相似线程池的保留站,让系统本身去动态调度先执行哪些指令。这个动态调度巧妙地解决了流水线阻塞的问题。指令执行的前后顺序,再也不和它们在程序中的顺序有关。咱们只要保证不破坏数据依赖就行了。CPU只要等到在指令结果的最终提交的阶段,再经过重排序的方式,确保指令“实际上”是顺序执行的。

相关文章
相关标签/搜索