数据依赖(一):流水线中的相关和冒险(Hazard)
在数字电路中也有冲突冒险的概念,也表示对公共资源的冲突,此文的冒险则侧重于流水线并行中的冒险概念。
现代 ISA 并非描述依赖的语言
数据相关即 Producer-Consumer 关系,后一个指令的输入依赖前一个指令的输入,根据因果性二者的执行必须串行。这种指令间的因果依赖关系要靠图结构来表示,每个节点都是一个指令。
但实际上现代 ISA 体系中并没有直接对应的图结构表示,我们依赖寄存器模型来描述依赖关系。寄存器模型对应存储的物理描述,而非依赖的逻辑描述,描述方式和处理问题之间的矛盾是历史上各种相关性讨论计数的基础。
至于为什么当初要选择使用寄存器模型,可能有历史原因,遵循图灵-冯诺依曼的大框架,也可能有实现原因,基于图结构描述的程序实现难度较高?
真相关和伪相关
依赖关系也可以表述为相关关系,前文提到相关对指令的执行顺序有要求,当实际执行和预期执行发生冲突,此时叫做“冒险”。可见产生冒险一定相关,相关不一定产生冒险。
预期执行依赖于相关关系,前文提到寄存器定义的 ISA 并不是描述依赖的语言,因此由寄存器提取的依赖关系可能并非是依赖,即伪相关。真相关一般分为数据相关(Producer-Consumer)、控制相关(if-else);伪相关包括名称相关,寄存器名称冲突导致的相关,可以通过寄存器重命名(如果剩余寄存器资源够的话)避免消除。这两种真相关对应的冒险则是数据冒险以及分支冒险。
实际执行还依赖于物理资源是否允许按照预期顺序执行,由物理资源引发的冲突叫做结构冒险。
流水线中的冒险
流水线中的冒险主要来自两部分,流水线中的 ILP 多条指令并行执行之间的冲突以及乱序破坏了原本编程顺序。
数据冒险
由于流水线并行俩个顺序指令挨着很近发生重叠,仍有可能冲突。数据关注 Producer 和 Consumer,即数据的输入和输出,在存储视角便是数据的读取和写入。流水线将各个部分划分,读取和存储往往划分在不同流水线阶段。
指令 B \ 指令 A | 输入(R) | 输出(W) |
---|---|---|
输入(R) | - | RAW |
输出(W) | WAR | WAW |
注意这些术语描述的是原本理想的逻辑术语,比如 RAW(Read After Write),指的是原本某指令应该在前一条指令写入后读取,但实际发生相反。即理想执行 RAW,实际执行 WAR。
那么什么时候会发生数据冒险呢?这便要结合具体流水线结构设计出发:
- 流水线基础共识,一般流水线写入阶段(Write-Back)往往在读取寄存器阶段后面(经典五级放在 ID 阶段,Instruction Decode);
- 程序中的顺序叫做编程顺序,按照编程顺序执行叫做顺序执行(In-Order),那么相反则是乱序执行(Out-of-Order);
- 随着流水线设计负责,可能读写不单单发生在一个阶段,一条流水线上可能有多个阶段发生对寄存器资源的同一种读或写操作。
基于读阶段发生在写阶段之后的基础共识,以及“是否顺序执行”、“是否多级写入”两个设计变量对三种数据冒险进行分析。首先由于写在读之后,只要顺序执行,WAR 冒险就不可能发生,WAR 仅当乱序执行(或者说动态调度)时会发生;同样由于写在读之后,有可能前一个指令还没在写,后一个指令就开始读了,RAW 冒险是一定有概率产生的;而 WAW 发生在顺序执行多级写入,或者乱序执行时。
写了一个程序 [1] 模拟数据冒险,其随机生成数据依赖指令在不同结构的流水线 Hazard 结果如下
分支冒险
即下一条指令依赖上一条指令的分支结果,一般可以引入分支预测解决。
结构冒险
在 pipeline 不同阶段对某个相同硬件资源访问冲突。
比如经典的五级流水线 IF-ID-EX-MEM-WB,各级对硬件资源的访问需求如下
Stage | Requirements |
---|---|
IF | 缓存读指令 |
ID | 读寄存器 |
EX | 数据总线 |
MEM | 缓存读写 |
WB | 写寄存器 |
比如如果 Register File 使用 Signal Port 实现,那么 ID 和 WB 可能会发生冒险,如果存在多发射结构,则 EX 多个执行单元会竞争数据总线等等。