想通过流水线设计来提升CPU的吞吐率,我们需要冒哪些风险。
流水线设计需解决的三大冒险:
- 结构冒险(Structural Hazard)
- 数据冒险(Data Hazard)
- 控制冒险(Control Hazard)
CPU流水线设计里,会遇到各种“危险”,使得流水线的下一条指令不能正常运行。但还是通过“抢跑”,“冒险”拿到一个提升指令吞吐率的机会。
流水线架构的CPU,是主动进行的冒险选择。期望能够通过冒险带来更高回报,所以,这不是无奈之下的应对之举,自然也算不上什么危机。
对于各种冒险可能造成的问题,其实都准备好了应对方案。
结构冒险
本质上是一个硬件层面的资源竞争问题,即一个硬件电路层面的问题。
CPU在同一个时钟周期,同时在运行两条计算机指令的不同阶段。但是这两个不同的阶段,可能会用到同样的硬件电路。
最典型的例子就是内存数据访问。
- 同一个时钟周期,两个不同指令访问同一个资源(5级流水线的示意图)
第1条指令执行到访存(MEM)时,流水线第4条指令,在执行取指令(Fetch)的操作。访存和取指令,都要进行内存数据的读取。内存只有一个地址译码器的作为地址输入,那就只能在一个时钟周期里读取一条数据,没法同时执行第1条指令的读取内存数据和第4条指令的读取指令代码。
类似的资源冲突最常见的就是薄膜键盘“锁键”。
薄膜键盘不是每一个按键背后都有独立线路,而是多个键共用一个线路。如果在同一时间,按下两个共用一个线路的按键,这两个按键信号就没法都传输出去。
重度键盘用户,都要买机械键盘或电容键盘。因为按键都有独立传输线路,“全键无冲”,大量写文章、写程序,还是打游戏,都不会按下键却没生效。
“全键无冲”本质就是增加资源。同样可用在CPU结构冒险。
对访问内存数据和取指令的冲突,把我们的内存分成两部分,各有各的地址译码器。这两部分分别是存放指令的程序内存和存放数据的数据内存。
这样把内存拆成两部分的解决方案,在计算机体系结构里叫作哈佛架构(Harvard Architecture)。
冯·诺依曼体系结构,又叫作普林斯顿架构(Princeton Architecture)。
如今的CPU仍是冯·诺依曼体系结构,并未将内存拆成程序内存、数据内存。
因为那样拆分,对程序指令和数据需要的内存空间,就无法根据实际应用去动态分配。虽然解决了资源冲突,但也失去灵活性。
现代CPU架构,借鉴了哈佛架构,在高速缓存层面拆分成指令缓存和数据缓存
不过,借鉴了哈佛结构的思路,现代的CPU虽然没有在内存层面进行对应的拆分,却在CPU内部的高速缓存部分进行了区分,把高速缓存分成了指令缓存(Instruction Cache)和数据缓存(Data Cache)两部分。
内存的访问速度远比CPU的速度要慢,所以现代的CPU并不会直接读取主内存。它会从主内存把指令和数据加载到高速缓存中,这样后续的访问都是访问高速缓存。而指令缓存和数据缓存的拆分,使得我们的CPU在进行数据访问和取指令的时候,不会再发生资源冲突的问题了。
结构冒险是一个硬件层面的问题,我们可以靠增加硬件资源的方式来解决。然而还有很多冒险问题,是程序逻辑层面的事儿。其中,最常见的就是数据冒险。
数据冒险:三种不同的依赖关系
同时在执行的多个指令之间,有数据依赖。
这些数据依赖,可分成三类:
- 先写后读(Read After Write,RAW)
- 先读后写(Write After Read,WAR)
- 写后再写(Write After Write,WAW)
先写后读(Read After Write)
C语言代码编译出来的汇编指令。
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。