CPU是如何解决冒险问题的?(上)

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

流水线设计需解决的三大冒险:

  • 结构冒险(Structural Hazard)
  • 数据冒险(Data Hazard)
  • 控制冒险(Control Hazard)


CPU流水线设计里,会遇到各种“危险”,使得流水线的下一条指令不能正常运行。但还是通过“抢跑”,“冒险”拿到一个提升指令吞吐率的机会。

流水线架构的CPU,是主动进行的冒险选择。期望能够通过冒险带来更高回报,所以,这不是无奈之下的应对之举,自然也算不上什么危机。


对于各种冒险可能造成的问题,其实都准备好了应对方案。

结构冒险

本质上是一个硬件层面的资源竞争问题,即一个硬件电路层面的问题。

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

最典型的例子就是内存数据访问。

  • 同一个时钟周期,两个不同指令访问同一个资源(5级流水线的示意图)

CPU是如何解决冒险问题的?(上)

第1条指令执行到访存(MEM)时,流水线第4条指令,在执行取指令(Fetch)的操作。访存和取指令,都要进行内存数据的读取。内存只有一个地址译码器的作为地址输入,那就只能在一个时钟周期里读取一条数据,没法同时执行第1条指令的读取内存数据和第4条指令的读取指令代码。


类似的资源冲突最常见的就是薄膜键盘“锁键”。

薄膜键盘不是每一个按键背后都有独立线路,而是多个键共用一个线路。如果在同一时间,按下两个共用一个线路的按键,这两个按键信号就没法都传输出去。

重度键盘用户,都要买机械键盘或电容键盘。因为按键都有独立传输线路,“全键无冲”,大量写文章、写程序,还是打游戏,都不会按下键却没生效。


“全键无冲”本质就是增加资源。同样可用在CPU结构冒险。

对访问内存数据和取指令的冲突,把我们的内存分成两部分,各有各的地址译码器。这两部分分别是存放指令的程序内存和存放数据的数据内存。


这样把内存拆成两部分的解决方案,在计算机体系结构里叫作哈佛架构(Harvard Architecture)。

冯·诺依曼体系结构,又叫作普林斯顿架构(Princeton Architecture)。


如今的CPU仍是冯·诺依曼体系结构,并未将内存拆成程序内存、数据内存。

因为那样拆分,对程序指令和数据需要的内存空间,就无法根据实际应用去动态分配。虽然解决了资源冲突,但也失去灵活性。

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。

上一篇:MFC——ComBox用法大全


下一篇:如何在遍历list,vector,map时删除符合条件的元素