大家好,欢迎来到IT知识分享网。
一、引子
1、流水线设计需要解决的三大冒险
过去两讲,我为你讲解了流水线设计CPU所需要的基本概念。接下来,我们一起来看看,要想通过流水线设计来提升CPU的吞吐率,我们需要冒哪些风险。
任何一本讲解CPU的流水线设计的教科书,都会提到流水线设计需要解决的三大冒险,分别是 结构冒险(Structural Harzard)、 数据冒险(Data Harzard)以及 控制冒险(Control Harzard)。
2、为什么在流水线设计里,harzard没有翻译成“危机”,而是要叫“冒险”呢?
事实上,对于各种冒险可能造成的问题,我们其实都准备好了应对的方案。这一讲里,我们先从结构冒险和数据冒险说起,一起来看看这些冒险及其对应的应对方案。
二、结构冒险:为什么工程师都喜欢用机械键盘?
我们先来看一看结构冒险。结构冒险,本质上是一个硬件层面的资源竞争问题,也就是一个硬件电路层面的问题。CPU在同一个时钟周期,同时在运行两条计算机指令的不同阶段。但是这两个不同的阶段,可能会用到同样的硬件电路。最典型的例子就是内存的数据访问。请你看看下面这张示意图,其实就是第20讲里对应的5级流水线的示意图。
1、没办法同时执行第1条指令的读取内存数据和第4条指令的读取指令代码
2、廉价的薄膜键盘共用一个线路
3、这也是为什么,重度键盘用户,都要买贵一点儿的机械键盘或者电容键盘
4、全键无冲”这样的资源冲突解决方案,其实本质就是 增加资源
“全键无冲”这样的资源冲突解决方案,其实本质就是 增加资源。同样的方案,我们一样可以用在CPU的结构冒险里面。对于访问内存数据和取指令的冲突,一个直观的解决方案就是把我们的内存分成两部分,让它们各有各的地址译码器。这两部分分别是 存放指令的程序内存和存放数据内存
5、我们今天使用的CPU并没有把内存拆成程序内存和数据内存这两部份
不过,借鉴了哈佛结构的思路,现代的CPU虽然没有在内存层面进行对应的拆分,却在CPU内部的高速缓存部分进行了区分,把高速缓存分成了 指令缓存(Instruction Cache)和 数据缓存(Data Cache)两部分。
内存的访问速度远比CPU的速度要慢,所以现代的CPU并不会直接读取主内存。它会从主内存把指令和数据加载到高速缓存中,这样后续的访问都是访问高速缓存。而指令缓存和数据缓存的拆分,使得我们的CPU在进行数据访问和取指令的时候,不会再发生资源冲突的问题了。
三、数据冒险:三种不同的依赖关系
结构冒险是一个硬件层面的问题,我们可以靠增加硬件资源的方式来解决。然而还有很多冒险问题,是程序逻辑层面的事儿。其中,最常见的就是数据冒险。
数据冒险,其实就是同时在执行的多个指令之间,有数据依赖的情况。这些数据依赖,我们可以分成三大类,分别是 先写后读(Read After Write,RAW)、 先读后写(Write After Read,WAR)和 写后再写(Write After Write,WAW)。下面,我们分别看一下这几种情况。
1、先写后读(Read After Write)
我们先来一起看看先写后读这种情况。这里有一段简单的C语言代码编译出来的汇编指令。这段代码简单地定义两个变量 a 和 b,然后计算 a = a + 2。再根据计算出来的结果,计算 b = a + 3。
代码
int main() { int a = 1; int b = 2; a = a + 2; b = a + 3; }
汇编
int main() { 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp int a = 1; 4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 int b = 2; b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 a = a + 2; 12: 83 45 fc 02 add DWORD PTR [rbp-0x4],0x2 b = a + 3; 16: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 19: 83 c0 03 add eax,0x3 1c: 89 45 f8 mov DWORD PTR [rbp-0x8],eax } 1f: 5d pop rbp 20: c3 ret
你可以看到,在内存地址为12的机器码,我们把0x2添加到 rbp-0x4 对应的内存地址里面。然后,在紧接着的内存地址为16的机器码,我们又要从rbp-0x4这个内存地址里面,把数据写入到eax这个寄存器里面。
所以,我们需要保证,在内存地址为16的指令读取rbp-0x4里面的值之前,内存地址12的指令写入到rbp-0x4的操作必须完成。这就是先写后读所面临的数据依赖。如果这个顺序保证不了,我们的程序就会出错。
这个先写后读的依赖关系,我们一般被称之为 数据依赖,也就是Data Dependency。
2、先读后写(Write After Read)
我们还会面临的另外一种情况,先读后写。我们小小地修改一下代码,先计算 a = b + a,然后再计算 b = a+ b。
代码
int main() { int a = 1; int b = 2; a = b + a; b = a + b; }
汇编
int main() { 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp int a = 1; 4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 int b = 2; b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 a = b + a; 12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 15: 01 45 fc add DWORD PTR [rbp-0x4],eax b = a + b; 18: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 1b: 01 45 f8 add DWORD PTR [rbp-0x8],eax } 1e: 5d pop rbp 1f: c3 ret
如果我们在内存地址18的eax的写入先完成了,在内存地址为15的代码里面取出 eax 才发生,我们的程序计算就会出错。这里,我们同样要保障对于eax的先读后写的操作顺序。
这个先读后写的依赖,一般被叫作 反依赖,也就是Anti-Dependency。
3、写后再写(Write After Write)
我们再次小小地改写上面的代码。这次,我们先设置变量 a = 1,然后再设置变量 a = 2。
代码
int main() { int a = 1; a = 2; }
汇编
int main() { 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp int a = 1; 4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 a = 2; b: c7 45 fc 02 00 00 00 mov DWORD PTR [rbp-0x4],0x2 }
所以,我们也需要保障内存地址4的指令的写入,在内存地址b的指令的写入之前完成。
这个写后再写的依赖,一般被叫作 输出依赖,也就是Output Dependency。
四、再等等:通过流水线停顿解决数据冒险
所以,我们需要有解决这些数据冒险的办法。其中最简单的一个办法,不过也是最笨的一个办法,就是流水线停顿(Pipeline Stall),或者叫流水线冒泡(Pipeline Bubbling)。
流水线停顿的办法很容易理解。如果我们发现了后面执行的指令,会对前面执行的指令有数据层面的依赖关系,那最简单的办法就是“ 再等等”。我们在进行指令译码的时候,
会拿到对应指令所需要访问的寄存器和内存地址。所以,在这个时候,我们能够判断出来,这个指令是否会触发数据冒险。如果会触发数据冒险,
我们就可以决定,让整个流水线停顿一个或者多个周期。
五、总结延伸
讲到这里,相信你已经弄明白了什么是结构冒险,什么是数据冒险,以及数据冒险所要保障的三种依赖,也就是数据依赖、反依赖以及输出依赖。
所以,下一讲,我们进一步看看,其他更高级的解决数据冒险的方案,以及控制冒险的解决方案,也就是操作数前推、乱序执行和还有分支预测技术。
转载于:https://www.cnblogs.com/luoahong/p/11436264.html
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/117790.html



