【22】冒险和预测(一):hazard是“危”也是“机”

【计算机组成原理】学习笔记——总目录

引言

要想通过流水线设计来提升 CPU 的吞吐率,我们需要冒哪些风险。

任何一本讲解 CPU 的流水线设计的教科书,都会提到流水线设计需要解决的三大冒险,分别是结构冒险(Structural Hazard)、数据冒险(Data Hazard)以及控制冒险(Control Hazard)。

在 CPU 的流水线设计里,固然我们会遇到各种“危险”情况,使得流水线里的下一条指令不能正常运行。但是,我们其实还是通过“抢跑”的方式,“冒险”拿到了一个提升指令吞吐率的机会。流水线架构的 CPU,是我们主动进行的冒险选择。我们期望能够通过冒险带来更高的回报,所以,这不是无奈之下的应对之举,自然也算不上什么危机了。

事实上,对于各种冒险可能造成的问题,我们其实都准备好了应对的方案。这一讲里,我们先从结构冒险和数据冒险说起,一起来看看这些冒险及其对应的应对方案。

一、结构冒险:为什么工程师都喜欢用机械键盘?

结构冒险,本质上是一个硬件层面的资源竞争问题,也就是一个硬件电路层面的问题。

CPU 在同一个时钟周期,同时在运行两条计算机指令的不同阶段。但是这两个不同的阶段,可能会用到同样的硬件电路。

最典型的例子就是内存的数据访问。请看下图:
在这里插入图片描述可以看到,在第 1 条指令执行到访存(MEM)阶段的时候,流水线里的第 4 条指令,在执行取指令(Fetch)的操作。访存和取指令,都要进行内存数据的读取。我们的内存,只有一个地址译码器的作为地址输入,那就只能在一个时钟周期里面读取一条数据,没办法同时执行第 1 条指令的读取内存数据和第 4 条指令的读取指令代码。

类似的资源冲突,其实你在日常使用计算机的时候也会遇到。最常见的就是薄膜键盘的“锁键”问题。常用的最廉价的薄膜键盘,并不是每一个按键的背后都有一根独立的线路,而是多个键共用一个线路。如果我们在同一时间,按下两个共用一个线路的按键,这两个按键的信号就没办法都传输出去。
这也是为什么,重度键盘用户,都要买贵一点儿的机械键盘或者电容键盘。因为这些键盘的每个按键都有独立的传输线路,可以做到“全键无冲”,这样,无论你是要大量写文章、写程序,还是打游戏,都不会遇到按下了键却没生效的情况。

“全键无冲”这样的资源冲突解决方案,其实本质就是增加资源。同样的方案,我们一样可以用在 CPU 的结构冒险里面。对于访问内存数据和取指令的冲突,一个直观的解决方案就是把我们的内存分成两部分,让它们各有各的地址译码器。这两部分分别是存放指令的程序内存存放数据的数据内存

计算机体系结构:
这样把内存拆成两部分的解决方案,在计算机体系结构里叫作哈佛架构(Harvard Architecture),来自哈佛大学设计Mark I 型计算机时候的设计。对应的,我们之前说的冯·诺依曼体系结构,又叫作普林斯顿架构(Princeton Architecture)。从这些名字里,我们可以看到,早年的计算机体系结构的设计,其实产生于美国各个高校之间的竞争中。

不过,我们今天使用的 CPU,仍然是冯·诺依曼体系结构的,并没有把内存拆成程序内存和数据内存这两部分。因为如果那样拆的话,对程序指令和数据需要的内存空间,我们就没有办法根据实际的应用去动态分配了。虽然解决了资源冲突的问题,但是也失去了灵活性
在这里插入图片描述
不过,借鉴了哈佛结构的思路,现代的 CPU 虽然没有在内存层面进行对应的拆分,却在 CPU 内部的高速缓存部分进行了区分,把高速缓存分成了指令缓存(Instruction Cache)和数据缓存(Data Cache)两部分。

内存的访问速度远比 CPU 的速度要慢,所以现代的 CPU 并不会直接读取主内存。它会从主内存把指令和数据加载到高速缓存中,这样后续的访问都是访问高速缓存。而指令缓存和数据缓存的拆分,使得我们的 CPU 在进行数据访问和取指令的时候,不会再发生资源冲突的问题了。

二、数据冒险:三种不同的依赖关系

结构冒险是一个硬件层面的问题,我们可以靠增加硬件资源的方式来解决。然而还有很多冒险问题,是程序逻辑层面的事儿。其中,最常见的就是数据冒险。

数据冒险,其实就是同时在执行的多个指令之间,有数据依赖的情况。这些数据依赖,我们可以分成三大类,分别是先写后读(Read After Write,RAW)、先读后写(Write After Read,WAR)和写后再写(Write After Write,WAW)。

先写后读【数据依赖Data Dependency】:读依赖于之前的写正常的依赖关系所以叫数据依赖。
先读后写【反依赖Anti-Dependency】:要保证读到写之前的数据,与正常的相反叫反依赖。
写后再写【输出依赖Data Dependency】:两次写操作顺序反了的话输出就错了。所以叫输出依赖。

三、再等等:通过流水线停顿解决数据冒险

除了读之后再进行读,你会发现,对于同一个寄存器或者内存地址的操作,都有明确强制的顺序要求。而这个顺序操作的要求,也为我们使用流水线带来了很大的挑战。因为流水线架构的核心,就是在前一个指令还没有结束的时候,后面的指令就要开始执行

所以,我们需要有解决这些数据冒险的办法。其中最简单的一个办法,不过也是最笨的一个办法,就是流水线停顿(Pipeline Stall),或者叫流水线冒泡(Pipeline Bubbling)。

流水线停顿的办法很容易理解。如果我们发现了后面执行的指令,会对前面执行的指令有数据层面的依赖关系,那最简单的办法就是“再等等”。我们在进行指令译码的时候,会拿到对应指令所需要访问的寄存器和内存地址。所以,在这个时候,我们能够判断出来,这个指令是否会触发数据冒险。如果会触发数据冒险,我们就可以决定,让整个流水线停顿一个或者多个周期
在这里插入图片描述我在前面说过,时钟信号会不停地在 0 和 1 之前自动切换。其实,我们并没有办法真的停顿下来。流水线的每一个操作步骤必须要干点儿事情。所以,在实践过程中,我们并不是让流水线停下来,而是在执行后面的操作步骤前面,插入一个== NOP 操作==,也就是执行一个其实什么都不干的操作
在这里插入图片描述这个插入的指令,就好像一个水管(Pipeline)里面,进了一个空的气泡。在水流经过的时候,没有传送水到下一个步骤,而是给了一个什么都没有的空气泡。这也是为什么,我们的流水线停顿,又被叫作流水线冒泡(Pipeline Bubble)的原因。

不过,流水线停顿这样的解决方案,是以牺牲 CPU 性能为代价的。因为,实际上在最差的情况下,我们的流水线架构的 CPU,又会退化成单指令周期的 CPU 了

所以,下一讲,我们进一步看看,其他更高级的解决数据冒险的方案,以及控制冒险的解决方案,也就是操作数前推、乱序执行和还有分支预测技术。

四、总结【个人总结的重点】

  • 通过流水线设计来提升 CPU 的吞吐率,需要冒风险:结构冒险(Structural Hazard)、数据冒险(Data Hazard)、控制冒险(Control Hazard)

  • 结构冒险的解决方案:增加硬件资源【高速缓冲:指令缓冲(Instruction Cache) 和 数据缓冲(Data Cache)】,让我们对于指令和数据的访问可以同时进行。这个办法帮助 CPU 解决了取指令和访问数据之间的资源冲突。

  • 计算机体系结构:哈佛架构(Harvard Architecture)、冯·诺依曼体系结构/普林斯顿架构(Princeton Architecture)

  • 数据冒险的解决方案:流水线停顿(Pipeline Stall)【又叫流水线冒泡(Pipeline Bubble)】,即增加NOP,执行一个其实什么都不干的操作。【此方案是直接进行等待。通过插入 NOP 这样的无效指令,等待之前的指令完成。这样我们就能解决不同指令之间的数据依赖问题。】

  • 数据冒险的三种依赖关系

    • 先写后读【数据依赖Data Dependency】:读依赖于之前的写正常的依赖关系所以叫数据依赖。
    • 先读后写【反依赖Anti-Dependency】:要保证读到写之前的数据,与正常的相反叫反依赖。
    • 写后再写【输出依赖Data Dependency】:两次写操作顺序反了的话输出就错了。所以叫输出依赖。

五、精选留言

提问:在采用流水线停顿的解决方案的时候,我们不仅要在当前指令里面,插入 NOP 操作,所有后续指令也要插入对应的 NOP 操作,这是为什么呢?
在这里插入图片描述

【计算机组成原理】学习笔记——总目录

猜你喜欢

转载自blog.csdn.net/sinat_40003796/article/details/126483587