为了提高处理器的性能,我们需要让每个时钟周期内发出多条指令,而不是只发出一条。这种多发射处理器有三种主要类型:
1. 静态调度的超标量处理器
2. VLIW(非常长指令字)处理器
3. 动态调度的超标量处理器。
这三种类型的处理器的区别在于它们每个时钟周期内发出的指令数量是固定的还是可变的,以及它们是按照指令的顺序执行还是乱序执行。VLIW处理器是靠编译器来安排指令并行执行的,而超标量处理器是靠硬件来安排指令并行执行的。静态调度的超标量处理器通常只能发出两条指令,所以当我们需要更多的并行度时,我们会选择VLIW或动态调度的超标量处理器。
基本 VLIW 方法
VLIW处理器有多个独立的功能单元,它们可以同时执行不同的操作。VLIW处理器的指令非常长,可以包含多个操作,或者要求指令包中的指令满足一定的约束。
VLIW处理器的优势在于它可以发出更多的指令,提高并行度。为了让功能单元忙碌起来,我们需要在代码中找到足够的并行性,这可以通过展开循环和调度代码来实现。如果展开循环后产生了没有分支的代码,那么我们可以用局部调度技术来安排指令。如果要跨越分支来调度代码,那么我们就需要用更复杂的全局调度算法来优化代码。全局调度算法不仅结构更复杂,而且还要处理更复杂的权衡问题,因为跨越分支会增加开销。
VLIW方法的优点是减少了硬件复杂性、降低了功耗、简化了解码和指令发射、提高了潜在的时钟频率。VLIW方法的缺点是需要复杂的编译器、增加了程序代码大小、需要更大的内存带宽和寄存器文件带宽、可能因为未预料到的事件(例如缓存未命中)而导致整个处理器停顿、可能因为VLIW中有空闲的操作码而浪费内存空间和指令带宽。
现在,我们先假设用展开循环就可以生成长而没有分支的代码序列,然后用局部调度来构造VLIW指令,并关注这些处理器的运行情况。
例子 假设我们有一个VLIW处理器,它每个时钟周期内可以发出两个内存引用、两个浮点操作和一个整数操作或分支。给出一个展开后的循环 x[i] = x[i] + s的版本,适合这样的处理器。展开多少次才能消除所有停顿。
上图显示了代码。循环被展开成了七个副本,这消除了所有停顿(即完全空闲的发射周期),并使得展开和调度后的循环在9个周期内运行完毕。这段代码的运行速率是9个周期内完成七个结果,即每个结果1.29个周期。
最初的VLIW模型有一些技术和物流方面的问题,使得这种方法的效率不高。技术方面的问题是代码大小的增加和锁步操作的限制。代码大小的增加有两个原因:一是为了在没有分支的代码片段中生成足够的操作,需要大胆地展开循环(就像前面的例子那样),从而增加了代码大小;二是当指令不满时,未使用的功能单元会在指令编码中浪费位数。
为了解决这个问题,有时会使用一些巧妙的编码方式。例如,可能只有一个大的立即数字段,供任何功能单元使用。另一种技术是在主存储器中压缩指令,在它们被读入缓存或解码时扩展它们。
早期的VLIW处理器是锁步操作的;它们完全没有危险检测硬件。这种结构要求任何功能单元管道中的停顿都必须导致整个处理器停顿,因为所有的功能单元必须保持同步。虽然编译器可能能够调度确定性的功能单元来避免停顿,但预测哪些数据访问会遇到缓存停顿并调度它们是非常困难的。因此,缓存需要是阻塞的,并导致所有功能单元停顿。随着发射率和内存引用数量的增加,这种同步限制变得难以接受。
在更近期的处理器中,功能单元更加独立地运行,编译器用于在发射时避免危险,而硬件检查允许发射后的指令不同步地执行。二进制代码兼容性也是通用VLIW或运行第三方软件的VLIW面临的一个主要物流问题。
在严格的VLIW方法中,代码序列既利用了指令集定义,又利用了详细的管道结构,包括功能单元和它们的延迟。因此,不同数量和延迟的功能单元需要不同版本的代码。这个要求使得在不同实现之间或不同发射宽度之间迁移变得比超标量设计更困难。当然,从一个新的超标量设计获得改进的性能可能需要重新编译。然而,能够运行旧的二进制文件是超标量方法的一个实际优势。
当并行性来自于FP程序中简单循环展开时,原始循环可能可以在向量处理器=上高效地运行。对于这样的应用程序,多发射处理器相比向量处理器是否有优势还不清楚;成本相似,而向量处理器通常速度相同或更快。多发射处理器相对于向量处理器潜在的优势是前者能够从不太结构化的代码中提取一些并行性,并且能够轻松地缓存所有形式的数据。