CPU对一条指令的执行,分为取指(IF), 指令译码/读寄存器(ID),执行(EX), 访存(MEM),写回寄存器(WB)五个阶段。对于多条指令,等待一条指令的五个阶段结束后再执行下一条指令就会导致效率非常低下。
举个例子,对于计算类指令,IF阶段需要访问主存和指令寄存器(IR),而EX阶段只需要ALU和其它通用寄存器。
现代计算机使用指令流水线,让CPU执行指令时,各个执行部件利用率尽量高。比如一条指令的IF在执行时需要访问内存和指令寄存器IR,与此同时可以让另一条指令处于EX阶段同时使用ALU。
但上图中流水线的安排只是理想情况,流水线中的指令可能会因为争用某个部件,或因为数据之间存在依赖,后一条指令需要上一条指令的执行结果,这些情况都会引起流水线的阻塞,这种现象称为流水线冒险。
流水线的冒险有三种情形,分别是结构冒险,数据冒险,控制冒险。
结构冒险
当流水线中的指令在执行时,需要访问同一部件时,会产生结构冒险。
下图中,指令1LOAD R1 [A]需要访问主存,而指令3的取址阶段也需要访问内存,因此出现了对内存资源的争用。产生了结构冒险
最简单的解决办法,我们可以让指令3暂停一个时钟周期,避开与指令1争用主存。
除此之外,第二种解决办法是,将数据和指令存放在独立的存储器中
以及第三种办法,取指令时,一次性取出多条指令放入指令队列中,后续的取指令从队列中取而非主存中。
数据冒险
在同一个程序中,下一条指令会用到上一条指令的结果,此时这两条指令在流水线中不等待直接执行,会产生数据冒险。
ADD R1 R2 ; 写R1
SUB R3 R1 ; 读R1
第二行的指令必须要在第一条指令完成WB阶段完成之后才能执行,否则会导致因为R1数据未更新而出错。
如果不加干预,会导致程序运行出错,它的流水线是这样的。
数据冒险根据读/写的顺序不同,分为下面三种
- 写后读(WAR),指令j会在指令i中写入之前读寄存器
- 读后写(RAW),指令j试图在指令i读出之前写入寄存器
- 写后写(WAW),指令j试图在指令i写之前写入寄存器,两次写的顺序被颠倒
首先我们发现,无论哪种冒险方式,都是写操作引起的冒险(类比一下两个进程同时只读共享一个文件是不会出错的)。所以我们可以让后面的读写操作后推到前一个写操作之后。这种方法称为后推法
我们用这个例子,来体会后推法
ADD R1, R2, R3 ; R1 = R2 + R3
SUB R4, R1, R5 ; R4 = R1 + R5
AND R6 R1, R2 ; R6 = R1 + R2
第二条,第三条指令,都被延后到了第一条指令的
WB阶段之后,这样就不会产生数据冒险
另一种方案是数据旁路技术,将指令的结果直接送到运算类指令AND等指令的EX段,而不用等到WB阶段,这样就能使流水线不发生停顿。
下图中第三条指令不必等到第五个时钟周期结束之后才进入ID阶段,通过数据旁路,对第三条AND指令发送信息,让它不必等待第一条指令的WB阶段结束
控制冒险
因为程序中存在JNE, JEQ等条件跳转指令,当执行条件跳转指令时,可能会修改程序计数器PC的值,也可能仅仅只是PC + 1 ➡️ PC。这种不确定性会导致流水线断流。流水线需要等条件跳转指令执行完成后才能恢复流水线。
为了解决控制冒险问题,可以尽早判断转移是否发生,尽早生成转移目标地址,预取成功和不成功两个方向上指令,加快和提前形成条件码,提高转移方向的准确率等等。
总结
结构冒险
指令同时争用一个硬件
解决办法
- 暂停一个时钟周期避免争用
- 将指令和数据放在独立的主存中
- 设置指令队列,减少取址阶段对主存的访问
数据冒险
后一条指令需要前一条指令的执行结果,如果不加干预会导致程序出错
解决办法
- 指令后推,等待前一条依赖指令写操作完成后再执行后一条指令
- 数据旁路,将指令执行的结果直接在
EX结束后送到后续指令EX阶段前,使其不用等待WB阶段完成就可以继续执行下去
控制冒险
因条件跳转语句导致指令必须等待条件跳转语句执行结束后才能确定下一条指导致流水线断流。
解决办法
- 尽早判断转移是否发生
- 尽早生成转移地址
- 同时读取两个转移方向上的目标指令
- 加快和提前条件码行程
- 提高转移方向猜测准确率