# G.2 深入分析向量性能
<!-- source: p1095 -->

对于长向量，chime 近似已经相当准确。不过，还有一种开销，影响远比指令发射限制更大。

chime 模型忽略的最重要开销，是向量启动时间。启动时间来自向量操作的流水线延迟，主要取决于所用功能单元的流水线深度。它使一个编队的实际执行时间超过一个 chime。由于我们假设各编队在时间上不重叠，启动时间还会推迟后续编队的执行。当然，相邻编队中的指令，不是在某个功能单元上存在结构冲突，就是存在数据依赖，因此不重叠的假设是合理的。

完成一个编队的实际时间，由向量长度与启动时间之和决定。如果向量长度无限大，启动开销就能被摊薄；但向量长度有限，启动开销便会显现，下面的例子说明了这一点。

**例题** 假设功能单元的启动开销如图 G.2 所示。给出各编队可以开始执行的时间，以及总共需要的周期数。向量长度为 64 时，该时间与 chime 近似相比如何？

**解答** 图 G.3 按编队给出答案，假定向量长度为 \(n\)。一个棘手的问题是：我们认为什么时刻才算整个向量指令序列完成？这决定了 `SV` 的启动时间是否可见。这里假设后续指令不能放入同一编队，而且前面已经假定编队之间不重叠。因此，总时间是最后一个编队中最后一条向量指令完成所需的时间。这是一种近似：最后一条向量指令的启动时间，在一些指令序列中可见，在另一些序列中则可能不可见。为简单起见，我们始终将它计入。

向量长度为 64 时，每个结果所需时间为 \(4+42/64=4.65\) 个时钟周期，而 chime 近似为 4 个时钟周期。计入启动开销后，执行时间是原来的 1.16 倍。

> **译注：** 上式沿用原书的 4.65；精确计算为 4.65625，通常保留两位小数应为 4.66。这里不把原书截取的近似值静默改成另一数值。

<!-- source: p1097 -->

| 单元 | 启动开销（周期） |
| --- | ---: |
| 加载和存储单元 | 12 |
| 乘法单元 | 7 |
| 加法单元 | 6 |
{#fig-G.2 .wide .expand num="G.2" caption="启动开销。"}

| 编队 | 开始时间 | 第一个结果时间 | 最后一个结果时间 |
| --- | --- | --- | --- |
| 1. `LV` | 0 | 12 | \(11+n\) |
| 2. `MULVS.D LV` | \(12+n\) | \(12+n+12\) | \(23+2n\) |
| 3. `ADDV.D` | \(24+2n\) | \(24+2n+6\) | \(29+3n\) |
| 4. `SV` | \(30+3n\) | \(30+3n+12\) | \(41+4n\) |
{#fig-G.3 .wide .expand num="G.3" caption="编队 1–4 的开始时间、第一个结果时间和最后一个结果时间。向量长度为 n。"}

为简单起见，我们通常使用 chime 近似估算运行时间；只有需要更精确的性能结果，或要说明某项改进的收益时，才计入启动时间。对长向量这一常见情形，启动开销的影响并不很大。本附录后面还会讨论降低启动开销的方法。

指令的启动时间来自执行该指令的功能单元的流水线深度。如果要保持每时钟周期产生一个结果的启动速率，则有：

$$
\text{流水线深度}=\left[\frac{\text{功能单元总执行时间}}{\text{时钟周期时间}}\right]
$$

例如，若一个操作需要 10 个时钟周期，那么要达到每周期启动一个操作的速率，就必须使用 10 级流水线。因此，流水线深度取决于操作的复杂度和处理器的时钟周期时间。功能单元的流水线深度差异很大，常见为 2–20 级；使用最频繁的单元通常为 4–8 个时钟周期。

对于 VMIPS，我们采用与 Cray-1 相同的流水线深度，尽管更现代的处理器往往具有更大的延迟，尤其是加载延迟。所有功能单元都采用完全流水化设计。按第 4 章的设定，浮点加法的流水线深度为 6 个时钟周期，浮点乘法为 7 个时钟周期。在 VMIPS 上，和大多数向量处理器一样，使用不同功能单元的独立向量操作可以在同一编队中发射。

除了启动开销，还要考虑执行 strip-mining 循环的开销。<!-- source: p1098 -->这种开销来自重新启动向量指令序列、设置向量长度寄存器（VLR）；在编队与其他指令不重叠的假设下，它实际上增加了向量启动时间。如果一个编队的这项开销为 10 个周期，那么每 64 个元素的有效开销就增加 10 个周期，即每元素增加 0.15 个周期。

由一系列编队组成的 strip-mining 循环，其运行时间主要取决于两个因素：

1. 循环中的编队数，它决定 chime 数。我们用 \(T_{\mathrm{chime}}\) 表示以 chime 为单位的执行时间。
2. strip-mining 中每组编队的开销。它由对每个数据块执行 strip-mining 标量代码的开销 \(T_{\mathrm{loop}}\)，以及各编队的向量启动开销 \(T_{\mathrm{start}}\) 组成。

第一次设置向量指令序列时，还可能有一项固定开销。近来的向量处理器已将它降得很低，因此这里忽略不计。

利用这些组成部分，可以表示一组向量指令处理长度为 \(n\) 的向量所需的总时间，记为 \(T_n\)：

$$
T_n=\left[\frac{n}{\mathrm{MVL}}\right]\times(T_{\mathrm{loop}}+T_{\mathrm{start}})+n\times T_{\mathrm{chime}}
$$

> **译注：** 原式使用普通方括号。结合后面把 200 个元素分成 4 段的例题，这里的方括号表示所需分段次数，即对 \(n/\mathrm{MVL}\) 向上取整；不是向下取整。

\(T_{\mathrm{start}}\)、\(T_{\mathrm{loop}}\) 和 \(T_{\mathrm{chime}}\) 的取值，都取决于编译器和处理器。指令的寄存器分配与调度，既会影响哪些指令被放入一个编队，也会影响各编队的启动开销。

为简单起见，VMIPS 的 \(T_{\mathrm{loop}}\) 采用常数。根据对 Cray-1 向量执行的多种测量，取 \(T_{\mathrm{loop}}=15\)。乍看之下，这个值似乎过小：每次循环都需要设置向量起始地址与步长、递增计数器，并执行循环分支。但实际上，这些标量指令可以与向量指令完全或部分重叠，从而尽量减少这些辅助操作占用的时间。当然，\(T_{\mathrm{loop}}\) 取决于循环结构；不过，与向量代码对 \(T_{\mathrm{chime}}\) 和 \(T_{\mathrm{start}}\) 的影响相比，这种依赖关系较弱。

| 操作 | 启动代价（周期） |
| --- | ---: |
| 向量加法 | 6 |
| 向量乘法 | 7 |
| 向量除法 | 20 |
| 向量加载 | 12 |
{#fig-G.4 .wide .expand num="G.4" caption="VMIPS 的启动代价。数值为 VMIPS 向量操作的启动时钟周期数。"}

<!-- source: p1099 -->

**例题** 在 VMIPS 上执行向量操作 \(A=B\times s\) 需要多长时间？其中 \(s\) 为标量，向量 \(A\)、\(B\) 的长度均为 200。

**解答** 假定 \(A\)、\(B\) 的地址最初分别位于 `Ra`、`Rb` 中，\(s\) 位于 `Fs` 中；还要记住，MIPS 及 VMIPS 的 `R0` 始终为 0。由于 \(200\bmod64=8\)，strip-mining 循环的第一次迭代处理 8 个元素，之后每次处理 64 个元素。每个向量下一段起始字节地址的增量，为本段向量长度的 8 倍。因此，第一段之后地址寄存器递增 \(8\times8=64\)，后续各段之后递增 \(8\times64=512\)。向量共有 \(8\times200=1600\) 字节；将下一段地址与初始地址加 1600 比较，即可判断是否完成。实际代码如下：

```text
      DADDUI  R2,R0,#1600  ; 向量总字节数
      DADDU   R2,R2,Ra    ; 向量 A 的结束地址
      DADDUI  R1,R0,#8    ; 第一段的长度
      MTC1    VLR,R1      ; 把向量长度写入 VLR
      DADDUI  R1,R0,#64   ; 第一段的字节长度
      DADDUI  R3,R0,#64   ; 其余各段的向量长度
Loop: LV      V1,Rb       ; 加载 B
      MULVS.D V2,V1,Fs    ; 向量乘标量
      SV      Ra,V2       ; 存储 A
      DADDU   Ra,Ra,R1    ; A 的下一段地址
      DADDU   Rb,Rb,R1    ; B 的下一段地址
      DADDUI  R1,R0,#512  ; 加载下一段的字节偏移量
      MTC1    VLR,R3      ; 将长度设为 64 个元素
      DSUBU   R4,R2,Ra    ; 是否已到 A 的末尾？
      BNEZ    R4,Loop     ; 若未到，则返回循环
```

循环中的三条向量指令存在依赖，必须放入三个编队，因此 \(T_{\mathrm{chime}}=3\)。代入基本公式：

$$
\begin{aligned}
T_n&=\left[\frac{n}{\mathrm{MVL}}\right]\times(T_{\mathrm{loop}}+T_{\mathrm{start}})+n\times T_{\mathrm{chime}}\\
T_{200}&=4\times(15+T_{\mathrm{start}})+200\times3\\
T_{200}&=60+4T_{\mathrm{start}}+600=660+4T_{\mathrm{start}}
\end{aligned}
$$

\(T_{\mathrm{start}}\) 是以下三项之和：

- 向量加载的 12 个时钟周期启动时间；
- 乘法的 7 个时钟周期启动时间；
- 存储的 12 个时钟周期启动时间。

因此：

$$
T_{\mathrm{start}}=12+7+12=31
$$

总执行时间为：

$$
T_{200}=660+4\times31=784
$$

<!-- source: p1100 -->

计入全部启动开销后，每元素执行时间为 \(784/200=3.9\) 个周期，而 chime 近似为 3 个周期。G.4 节将进一步允许不同编队重叠执行。

图 G.5 给出前例 \(A=B\times s\) 在不同向量长度下的每元素开销与实际执行速率。按 chime 计数的模型，每元素需要 3 个时钟周期；两种开销在极限情况下还会为每元素增加 0.9 个时钟周期。

![向量长度对每元素总执行时间和开销的影响](/figures/app-g/fig-g-5.png)
{#fig-G.5 .wide .expand num="G.5" caption="原书 F-6 页例题中，每元素总执行时间及总开销随向量长度的变化。对短向量，总启动时间超过总时间的一半；对长向量，降至约三分之一。曲线突跳发生在向量长度越过 64 的倍数时，此时必须多执行一次 strip-mining 循环及一组向量指令，使 Tn 增加 Tloop＋Tstart。"}

图内术语：Clock cycles＝时钟周期；Vector size＝向量长度；Total time per element＝每元素总时间；Total overhead per element＝每元素总开销。

> **译注：** 图注原写“F-6 页”，对应例题实际位于本附录 G-6 页。正文“极限情况下增加 0.9 个周期”也与本节参数不一致：按 \(T_{\mathrm{loop}}=15\)、\(T_{\mathrm{start}}=31\)、\(\mathrm{MVL}=64\)，长向量的每元素开销极限为 \((15+31)/64\approx0.719\) 个周期。

## 指令启动的流水化与多通道

增加通道数可以提高峰值性能，却不会改变启动延迟。因此，让一条向量指令的启动与此前向量指令的收尾重叠，以减少启动开销，就变得十分关键。最简单的情况是两条指令访问不同的向量寄存器。例如：

```text
ADDV.D V1,V2,V3
ADDV.D V4,V5,V6
```

<!-- source: p1101 -->

一种实现方式是：第一条向量指令的最后一个元素进入浮点加法流水线后，紧接着让第二条指令的第一个元素进入。为降低控制逻辑复杂度，一些向量机要求两条派发到同一向量单元的指令之间，保留一段恢复时间，即**空转时间**（dead time）。图 G.6 用单条向量流水线的时序图，同时说明启动延迟与空转时间。

下面的例题说明空转时间对可达到的向量性能有何影响。

**例题** Cray C90 有两条通道，但发往同一功能单元的任意两条向量指令之间，即使没有数据依赖，也必须有 4 个时钟周期的空转时间。最大向量长度为 128 个元素时，这会使可达到的峰值性能降低多少？如果通道数增至 16，又会降低多少？

**解答** 最大长度为 128 的向量分配到两条通道，需占用向量功能单元 64 个时钟周期。空转时间又增加 4 个周期的占用，使峰值性能降至无空转时间时的 \(64/(64+4)=94.1\%\)。如果增至 16 条通道，最大长度的向量指令只需占用功能单元 \(128/16=8\) 个周期，空转时间使峰值性能降至无空转时间时的 \(8/(8+4)=66.6\%\)。后一种情况下，向量单元的忙碌时间永远不可能超过总时间的三分之二！

![单条向量流水线的启动延迟与空转时间](/figures/app-g/fig-g-6.png)
{#fig-G.6 .wide .expand num="G.6" caption="单条向量流水线的启动延迟与空转时间。每个元素的延迟为 5 个周期：1 个周期读向量寄存器堆、3 个周期执行、1 个周期写向量寄存器堆。同一向量指令的各元素可连续进入流水线，但这台机器在不同向量指令之间插入 4 个空转周期。更复杂的控制逻辑可以消除空转时间。经许可转载自 Asanovic [1998]。"}

图内术语：Start-up latency＝启动延迟；First／Second vector instruction＝第一／第二条向量指令；Dead time／Dead cycle＝空转时间／空转周期；Element＝元素；R、X1–X3、W 分别表示读寄存器、三个执行级和写寄存器。

<!-- source: p1102 -->

当多条指令可能读写同一个向量寄存器，或某些指令可能因不可预测的原因停顿——例如向量加载遇到存储体冲突——时，指令启动的流水化就更加复杂。但随着通道数与流水线延迟不断增加，允许指令启动完全流水化，也就愈加重要。
