4. 超标量架构:指令级并行(ILP)的极致挖掘,多发射与动态调度
各位同学,今天我们聊点硬核的——超标量架构。
单发射流水线,说白了就是一个工人干活,一次只能处理一条指令。你想想看,流水线再深,每个时钟周期也只能完成一条指令。那能不能一次多发几条?当然可以。这就是超标量的核心思想:一个时钟周期内发射多条指令。
我当年刚接触超标量时,觉得这玩意儿不就是把发射宽度从1变成2、4、8嘛,有什么难的?结果第一次做项目就被狠狠教育了。嗯,这里面的坑,咱们一个一个说。
4.1 多发射:从单车道到八车道
超标量处理器,说白了就是把单车道变成了多车道。每个时钟周期,取指单元从指令缓存中取出多条指令,然后同时发射到多个执行单元去。
举个例子,一个4发射的处理器,每个周期最多可以处理4条指令。理想情况下,IPC(每周期指令数)可以接近4。但现实很骨感——指令之间是有依赖关系的。
4.1.1 静态多发射 vs 动态多发射
多发射有两种实现方式,我分别说说我的理解。
静态多发射:编译器负责把可以并行执行的指令打包成“发射包”。硬件比较简单,但编译器压力大。我记得在早期的MIPS处理器上见过这种设计,编译器得费老大劲去调度指令。
动态多发射:硬件自己判断哪些指令可以并行发射。这需要复杂的发射逻辑,但兼容性好,对编译器要求低。现在的主流处理器,比如ARM的Cortex-X系列、Intel的Core系列,用的都是动态多发射。
我个人更倾向于动态多发射。为什么?因为编译器再聪明,也猜不到运行时的情况。比如缓存缺失、分支预测错误,这些动态事件只有硬件能处理。
4.2 动态调度:让硬件自己找活干
动态调度,就是硬件在运行时重新排列指令的执行顺序,以克服数据依赖带来的停顿。
你可能会问:指令不是按顺序发射的吗?怎么还能重排?
答案是:指令可以乱序执行。只要不影响最终结果,先执行哪条后执行哪条都行。
4.2.1 数据依赖的三种类型
要理解动态调度,先得搞清楚指令之间的依赖关系。我总结了一张表:
| 依赖类型 | 含义 | 能否绕过 |
|---|---|---|
| RAW(真依赖) | 后一条指令读前一条指令写的结果 | 不能绕过,必须等 |
| WAR(反依赖) | 后一条指令写前一条指令读的寄存器 | 可以通过寄存器重命名绕过 |
| WAW(输出依赖) | 两条指令写同一个寄存器 | 可以通过寄存器重命名绕过 |
我曾经在一个项目中,因为没处理好WAR依赖,导致数据冒险,调试了整整三天。后来发现,寄存器重命名就是专门解决这个问题的。
4.2.2 Tomasulo算法:经典中的经典
说到动态调度,就不得不提Tomasulo算法。这是IBM 360/91上用的技术,距今快60年了,但思想依然在用。
核心思路:
- 保留站(Reservation Station):每条指令发射前,先放到保留站里等操作数。操作数准备好了,就自动发射执行。
- 寄存器重命名:通过硬件把逻辑寄存器映射到物理寄存器,消除WAR和WAW依赖。
- 公共数据总线(CDB):执行结果通过CDB广播给所有等待的指令,实现数据转发。
说白了,Tomasulo算法就是让硬件自己管理指令的发射和执行顺序,不需要编译器操心。
4.3 超标量处理器的核心结构
一个典型的超标量处理器,内部结构大致如下:
这张图我画了好一会儿。你看,从取指到提交,每个阶段都是多路的。但最关键的其实是发射队列和提交单元这两个模块。
4.3.1 发射队列:指令的等待室
发射队列(也叫保留站)是动态调度的核心。每条指令译码后,先进入发射队列。队列里的指令等待操作数就绪,一旦就绪,就发射到对应的执行单元。
这里有个关键设计:发射队列的深度。深度越大,能容纳的指令越多,找到可并行指令的概率就越高。但深度大了,硬件开销也大。我见过一个设计,发射队列深度从32增加到64,面积增加了30%,但性能只提升了5%。
4.3.2 重排序缓冲(ROB):乱序执行,按序提交
乱序执行虽然能提高性能,但带来了一个麻烦:异常处理。如果指令A先执行,指令B后执行,但指令A发生了异常,怎么办?
答案是:ROB(Reorder Buffer)。所有指令执行完后,先写到ROB里,然后按原始程序顺序提交。只有提交时,结果才真正写入寄存器或内存。这样,异常发生时,可以精确地回滚到异常点之前的状态。
我曾经在调试一个乱序执行处理器的bug时,发现ROB的指针管理出了问题,导致提交顺序错乱,程序跑飞了。嗯,从那以后,我对ROB的验证格外小心。
4.4 实际设计中的挑战
超标量架构看着美好,但实际设计时,处处是坑。
4.4.1 发射逻辑的复杂度
假设一个4发射的处理器,发射队列里有32条指令。每次发射时,要从32条指令里选出4条操作数就绪的指令。这个选择逻辑,用硬件实现起来非常复杂。
我见过一个设计,发射逻辑占了整个核心面积的15%。为了降低复杂度,有些设计采用了分簇发射——把发射队列分成几个小簇,每个簇独立选择指令。
4.4.2 寄存器文件端口数
多发射意味着每个周期要读多个寄存器的值。比如4发射的处理器,每个周期可能需要读8个寄存器(每条指令2个源操作数),写4个寄存器。这需要多端口寄存器文件。
端口数多了,寄存器文件的面积和功耗会爆炸式增长。我记得有个项目,因为寄存器文件端口数太多,导致布局布线无法收敛,最后不得不降低发射宽度。
4.4.3 分支预测的准确性
超标量处理器对分支预测的准确性要求极高。为什么?因为一旦预测错误,前面发射的所有指令都要冲刷掉。4发射的处理器,如果分支预测错误,一次就浪费了4个周期的指令。
我建议在设计中,分支预测器的准确率至少要达到95%以上,否则超标量的优势会被分支惩罚抵消掉。
4.5 小结
超标量架构,说白了就是用硬件复杂度换取指令级并行度。多发射提供了并行发射的能力,动态调度解决了数据依赖的问题,ROB保证了精确异常。
但这一切都是有代价的——面积、功耗、设计复杂度。在实际项目中,你需要根据应用场景来权衡:是做4发射还是8发射?发射队列深度设多少?寄存器文件用多少端口?
嗯,这些问题的答案,没有标准解。只有通过大量的仿真和验证,才能找到最适合你设计的方案。
公众号:蓝海资料掘金营,微信 deep3321