# 3.9 利用多发射与静态调度开发 ILP
<!-- source: p0257 -->

前面各节介绍了多发射处理器利用硬件开发 ILP 的技术。尽管编译程序时假设指令顺序执行，一次一条、互不重叠，硬件仍利用分支预测、寄存器重命名和动态调度来发现并开发 ILP。相比之下，**VLIW 处理器**每次发射固定数量的指令，它们被组织成一条大指令，或一个固定的指令包，并在指令中明确标出并行性。VLIW 天生由编译器静态调度。Intel 和 HP 创建附录 H 所述的 IA-64 体系结构时，还为这种体系结构风格引入了 **EPIC** 名称，即 explicitly parallel instruction computer，显式并行指令计算机。

## 基本 VLIW 方法

VLIW 使用多个独立功能单元。它不试图分别向这些单元发射多条独立指令，而是把多个操作打包成一条很长的指令，或者要求发射包内的指令满足相同约束。两种方式没有本质区别，因此下面沿用最初的 VLIW 方法，假设多个操作放在同一条指令中。

VLIW 的优势随最大发射率增加而扩大，因此我们重点考察更宽的发射。事实上，对于简单双发射处理器，超标量方式的额外开销可能很小。许多设计者也会认为四发射处理器的开销仍可控制，但如本章前面所示，开销增长是限制进一步加宽发射的主要因素。

考虑一个 VLIW 处理器，每条指令包含五个操作：一个整数操作，也可以是分支；两个浮点操作；两次内存访问。指令为每个功能单元保留一组字段，每个单元可能需要 16～24 位，总指令长度为 80～120 位。相比之下，Intel Itanium 1 和 2 每个指令包包含六个操作，即可以同时发射两个各含三条指令的指令束，详见附录 H。

<!-- source: p0258 -->

为了使功能单元保持忙碌，代码序列必须有足够并行性，填满可用操作槽。通过循环展开，并在扩大的单个循环体内调度代码，就能发现这种并行性。如果展开产生直线代码，可以采用只处理单个基本块的局部调度技术。如果要跨越分支调度，才能发现并开发并行性，就必须使用复杂得多的全局调度算法。全局调度不仅结构更复杂，还要处理复杂得多的优化取舍，因为跨分支移动代码代价很高。

附录 H 将介绍**踪迹调度**（trace scheduling），这是一种专为 VLIW 开发的全局调度技术；还会介绍特殊硬件支持，用来消除部分条件分支，扩展局部调度的适用范围，并提高全局调度的性能。

目前，我们先依靠循环展开，生成较长的直线代码，再用局部调度组成 VLIW 指令，重点考察这类处理器的执行效果。

**例题** 假设一个 VLIW 每周期可发射两次内存访问、两个浮点操作，以及一个整数操作或分支。为它给出循环 \(x[i]=x[i]+s\) 的展开版本，RISC-V 代码见 3.2 节。充分展开，以消除所有停顿。[^section-reference]

**解答** 图 3.28 给出代码。循环展开为七份循环体，消除了所有停顿，即完全空闲的发射周期。展开并调度后的循环需要 9 个周期，因此每 9 周期产生 7 个结果，平均每个结果 1.29 周期，几乎是 3.2 节采用展开、调度代码的双发射超标量处理器的两倍速度。[^section-reference]

原始 VLIW 模型在技术和实际使用层面都遇到问题，影响了效率。技术问题主要是代码增大，以及同步锁步运行的限制。两个因素共同导致 VLIW 代码显著增大。第一，要在直线代码片段中产生足够操作，就需要像前面的例子那样大量展开循环，从而扩大代码。第二，只要指令没填满，未使用功能单元对应的编码位就被浪费。附录 H 将讨论软件流水线等软件调度方法，在不显著扩大代码的情况下，获得循环展开的收益。

为抑制代码增大，有时会采用巧妙编码。例如，只设置一个较大的立即数字段，供任意功能单元使用。另一种方法是在主存中压缩指令，读入缓存或译码时再展开。附录 H<!-- source: p0259 -->将介绍其他技术，并记录 IA-64 中明显的代码膨胀。

| 内存访问 1 | 内存访问 2 | 浮点操作 1 | 浮点操作 2 | 整数操作／分支 |
| --- | --- | --- | --- | --- |
| `fld f0,0(x1)` | `fld f6,-8(x1)` |  |  |  |
| `fld f10,-16(x1)` | `fld f14,-24(x1)` |  |  |  |
| `fld f18,-32(x1)` | `fld f22,-40(x1)` | `fadd.d f4,f0,f2` | `fadd.d f8,f6,f2` |  |
| `fld f26,-48(x1)` |  | `fadd.d f12,f0,f2` | `fadd.d f16,f14,f2` |  |
|  |  | `fadd.d f20,f18,f2` | `fadd.d f24,f22,f2` |  |
| `fsd f4,0(x1)` | `fsd f8,-8(x1)` | `fadd.d f28,f26,f24` |  |  |
| `fsd f12,-16(x1)` | `fsd f16,-24(x1)` |  |  | `addi x1,x1,-56` |
| `fsd f20,24(x1)` | `fsd f24,16(x1)` |  |  |  |
| `fsd f28,8(x1)` |  |  |  | `bne x1,x2,Loop` |
{#fig-3.28 .wide .expand num="3.28" caption="替代展开循环序列的 VLIW 指令"}

**图 3.28** 构成内层循环、替代原展开序列的 VLIW 指令。假设分支预测正确，该代码需要 9 个周期。9 周期发射 23 个操作，即每周期 2.5 个。效率，即包含实际操作的可用槽位比例，约为 60%。[^fig328-errors] 要达到这一发射率，需要比 RISC-V 通常在该循环中使用的更多寄存器。上述 VLIW 代码至少需要 8 个浮点寄存器；相应的基础 RISC-V 代码最少可用 2 个浮点寄存器，展开、调度后可能需要 5 个。

早期 VLIW 以锁步方式运行，完全没有冒险检测硬件。这种结构要求任意功能单元流水线停顿时，整个处理器都必须停顿，因为所有功能单元必须同步。编译器也许能对延迟确定的功能单元进行调度，以避免停顿，但很难预测哪些数据访问会因缓存而停顿，并据此调度。因此，缓存必须是阻塞式的，一旦阻塞就使所有功能单元停顿。随着发射率与内存访问数量增加，这种同步限制变得不可接受。较新的处理器让功能单元更加独立：由编译器避免发射时的冒险，而指令发射后，通过硬件检查允许不同步执行。

对于通用 VLIW 或需要运行第三方软件的 VLIW，二进制代码兼容性也是重大实际问题。在严格 VLIW 方案中，代码序列不仅依赖指令集定义，还依赖具体流水线结构，包括功能单元和延迟。因此，功能单元数量与延迟不同，就需要不同代码版本。这使跨代实现，或不同发射宽度实现之间的迁移，比超标量设计更困难。当然，要充分利用新超标量设计的性能，也可能需要重新编译；不过，能够直接运行旧二进制文件，仍是超标量方案的一项实际优势。在第 7 章介绍的领域专用体系结构中，<!-- source: p0260 -->这个问题不严重，因为应用就是针对特定体系结构配置编写的。

以 IA-64 为主要代表的 EPIC，为早期通用 VLIW 设计的许多问题提供了解决办法，包括支持更激进软件推测的扩展，以及在保留二进制兼容性的同时，克服代码依赖具体硬件的限制。

## 多发射的整体认识

图 3.29 总结多发射的基本方法、区别性特征，以及采用各方案的处理器。静态调度超标量处理器每周期发射的指令数虽可变，而非固定，但在概念上其实更接近 VLIW，因为两者都依靠编译器为处理器调度代码。随着发射宽度增加，静态调度超标量的优势递减，因此主要用于较窄发射，通常仅为双发射。若干低功耗处理器，例如本章后面介绍的 Arm A53，就是双发射、静态调度超标量处理器。超过这个宽度，大多数设计者会选择 VLIW，或动态调度并支持推测的超标量方案。

EPIC 体系结构在 2020 年取消之后，动态、推测超标量处理器成为服务器、桌面、笔记本乃至手机系统的主流。因此，“超标量处理器”通常就是指动态调度、支持推测执行的设计。有些芯片采用称为 **big.LITTLE** 的组合，同时包含动态调度与静态调度超标量处理器，交由软件根据使用场景，选择哪种处理器以优化性能或降低功耗。VLIW 在信号处理应用中仍然流行；第 7 章讨论领域专用加速器时，还会再次遇到它们。

所有多发射处理器的主要挑战，都是开发大量 ILP。如果并行性来自对浮点程序中简单循环的展开，那么原循环很可能也能在下一章介绍的向量处理器上高效运行。对这类应用，多发射处理器是否优于向量处理器并不明确：成本相近，而向量处理器通常同样快，甚至更快。多发射处理器的潜在优势，是能从结构较不规则的代码中提取一定并行性，并容易对各种形式的数据使用缓存。因此，多发射已成为开发 ILP 的主要方法，向量则主要成为这类处理器的扩展。

<!-- source: p0261 -->

| 通用名称 | 发射组织 | 冒险检测 | 调度 | 区别性特征 | 示例 |
| --- | --- | --- | --- | --- | --- |
| 静态超标量 | 动态 | 硬件 | 静态 | 顺序执行 | 主要在嵌入式领域：MIPS、ARM，包括 Cortex-A53 |
| 动态超标量 | 动态 | 硬件 | 动态 | 部分乱序执行，但不推测 | 目前没有 |
| 超标量（动态并推测） | 动态 | 硬件 | 带推测的动态调度 | 带推测的乱序执行 | Intel Xeon 和 Core、AMD EPYC 和 Ryzen、ARM Neoverse 及许多 Cortex-A、Apple M1 |
| VLIW／LIW | 静态 | 主要依靠软件 | 静态 | 所有冒险由编译器确定并指示，通常为隐式指示 | 多见于信号处理，如 TI C6x |
| EPIC | 主要为静态 | 主要依靠软件 | 主要为静态 | 所有冒险由编译器确定并显式指示 | Intel Itanium |
{#fig-3.29 .wide .expand num="3.29" caption="多发射处理器的五种主要方法"}

**图 3.29** 多发射处理器使用的五种主要方法，以及区分它们的主要特征。本章侧重依赖硬件的技术，它们都是某种形式的超标量；附录 H 侧重编译器方法。IA-64 所体现的 EPIC 扩展了早期 VLIW 的许多概念，融合了静态与动态方法。

[^section-reference]: 译注：解答称“3.2 节的双发射超标量”，而 3.2 节所列展开调度按每周期一条指令计时。
[^fig328-errors]: 译注：原表第 4 行写 `fadd.d f12,f0,f2`，与第 3 个元素加载到 `f10` 不符；第 6 行写 `fadd.d f28,f26,f24`，加数不是其他元素使用的标量 `f2`。原图注效率为“约 60%”，但按所示 9 周期、每周期 5 槽、23 个操作计算，应为 \(23/(9\times5)\approx51.1\%\)。\(9/7\approx1.29\) 周期／结果的算术本身成立。
