参考来源:《超标量处理器设计》—— 姚永斌
超标量处理器
一个程序执行时间的公式如下,而这个公式通常也反映了处理器的性能:
图中的CPI - Cycle Per Instruction也就是CPU每条指令需要的周期数量,CPI计算方法就是周期数量除以这段周期内的指令数量。
而基于CPI,就有IPC - Instructions Per Cycle参数,定义执行一条指令需要多少时钟周期,属于更为通用的定义处理器性能的参数。
通常有以下几个加快处理器速度方法:
- 减少指令数量
- 减少CPI,让每个周期执行更多指令,也就是增加IPC,让一个指令的执行时间更短
- 超频,减少单个周期时间
而超标量处理器就是通过在同一个时间内并行执行n条指令来提高IPC的n-way处理器,拥有以下优势:
- 可以通过硬件或者软件配合编译器支持决定哪些指令可以并行执行
- 每个周期可以从Instruction Cache中取出n条指令送到CPU流水线,CPU每个周期也可以执行n条指令(n-way超标量处理器)
- 支持超长指令字(Very Long Instruction Word, VLIW)
后续文章中会围绕超标量流水线处理器的设计进行分享,这里先对于Pipeline进行分享。
Pipeline
流水线设计是超标量处理器中基础的一环,理想情况下,流水线具有以下特征:
- 每个阶段所需要的时间近似
- 每个阶段操作重复执行(实际存在依赖空档期)
- 各个操作阶段之间相互独立、互不干扰(实际RAW场景是相互依赖的)
假设处理器没有使用流水线的周期为D,频率为1/D。在n级流水线中周期为"D/n + S",S为流水线寄存器的延时,变化信息如下所示:
可以看到,流水线在延时S不影响整体的前提下,能有效提高处理器频率。
当然,更多的流水线设计也会更多消耗硬件面积资源。
不同指令集的流水线实现复杂度也不同:
CISC指令长度以及执行时间不等,实现流水线较为复杂。
RISC指令长度相等,指令执行任务较为规整,比较容易实现流水线。
RISCV流水线如下如图所示:
阶段 | 任务 |
---|---|
Fetch | 取指令,使用PC寄存器的数值作为地址,从I-Cache取指令并存储在指令寄存器中 |
Decode & RegFile read | 指令解码,根据结果读取寄存器堆(register file)获取指令源操作数 |
Execute | 根据指令类型,完成计算任务。例如算数类型进行算数运算,访存类型完成地址计算 |
Memory | 访问D-Cache,主要面向读load/写store指令,其他指令类型此阶段不执行 |
Write Back | 若指令存在目的寄存器,将最终结果写到目的寄存器 |
流水线指令相关性
RAW-先写后读
Read指令操作数来自之前指令Write的,此场景下是强依赖关系。
Write: R1 = R2 + R3
Read: R5 = R1 + R4 // R5结果依赖R1数值
WAR-先读后写
Write指令需要将结果写到某个寄存器中,但是这个寄存器还在被其他指令Read,无法立即写入。
Read: R1 = R2 + R3
Write: R2 = R5 + R4 // R2状态受到之前的访问限制
上述这种情况都可以通过修改下一条指令写入位置(不写入R2寄存器)避免依赖。
WAW-先写后写
Write: R1 = R2 + R3
Write: R1 = R5 + R4
这种依赖性也是可以通过将第二条指令写入其他寄存器避免。
还有一些指令的相关性类型如下:
控制相关性
分支指令引起,只有分支结果确定后,才知道从哪里取得后续指令执行。只能通过预测的方法取指。
存储器相关性
当两个寄存器存储的数值来源的内存地址一样时,会存在隐蔽的相关性。
sw r1, 0(r5) // 将寄存器r1数值保存到MEM[r5]
lw r2, 0(r6) // 将MEM[r6]数值读取到r2寄存器
直接看上面的指令很难看出两条指令的相关性,但实际上r5寄存器数值和r6寄存器数值都来自于一块内存。
那么第二条指令的读取,必须等待第一条指令的写入才可以,即RAW的依赖。
而这些指令相关性都会阻碍超标量处理器中的指令乱序执行,都需要在流水线中进行特殊处理,后续会在这篇文章介绍。