跳转到主要内容

G.2 深入分析向量性能

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

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

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

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

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

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

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

单元启动开销(周期)
加载和存储单元12
乘法单元7
加法单元6
图 G.2 启动开销。
编队开始时间第一个结果时间最后一个结果时间
1. LV01211+n11+n
2. MULVS.D LV12+n12+n12+n+1212+n+1223+2n23+2n
3. ADDV.D24+2n24+2n24+2n+624+2n+629+3n29+3n
4. SV30+3n30+3n30+3n+1230+3n+1241+4n41+4n
图 G.3 编队 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 循环的开销。这种开销来自重新启动向量指令序列、设置向量长度寄存器(VLR);在编队与其他指令不重叠的假设下,它实际上增加了向量启动时间。如果一个编队的这项开销为 10 个周期,那么每 64 个元素的有效开销就增加 10 个周期,即每元素增加 0.15 个周期。

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

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

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

利用这些组成部分,可以表示一组向量指令处理长度为 nn 的向量所需的总时间,记为 TnT_n:

Tn=[nMVL]×(Tloop+Tstart)+n×Tchime T_n=\left[\frac{n}{\mathrm{MVL}}\right]\times(T_{\mathrm{loop}}+T_{\mathrm{start}})+n\times T_{\mathrm{chime}}

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

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

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

操作启动代价(周期)
向量加法6
向量乘法7
向量除法20
向量加载12
图 G.4 VMIPS 的启动代价。数值为 VMIPS 向量操作的启动时钟周期数。

例题 在 VMIPS 上执行向量操作 A=B×sA=B\times s 需要多长时间?其中 ss 为标量,向量 AA、BB 的长度均为 200。

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

      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     ; 若未到,则返回循环

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

Tn=[nMVL]×(Tloop+Tstart)+n×TchimeT200=4×(15+Tstart)+200×3T200=60+4Tstart+600=660+4Tstart \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}

TstartT_{\mathrm{start}} 是以下三项之和:

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

因此:

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

总执行时间为:

T200=660+4×31=784 T_{200}=660+4\times31=784

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

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

向量长度对每元素总执行时间和开销的影响
图 G.5 原书 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 个周期”也与本节参数不一致:按 Tloop=15T_{\mathrm{loop}}=15、Tstart=31T_{\mathrm{start}}=31、MVL=64\mathrm{MVL}=64,长向量的每元素开销极限为 (15+31)/64≈0.719(15+31)/64\approx0.719 个周期。

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

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

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

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

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

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

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

单条向量流水线的启动延迟与空转时间
图 G.6 单条向量流水线的启动延迟与空转时间。每个元素的延迟为 5 个周期:1 个周期读向量寄存器堆、3 个周期执行、1 个周期写向量寄存器堆。同一向量指令的各元素可连续进入流水线,但这台机器在不同向量指令之间插入 4 个空转周期。更复杂的控制逻辑可以消除空转时间。经许可转载自 Asanovic [1998]。

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

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

表格阅读