# B.2 缓存性能
<!-- source: p0738 -->

指令数与硬件无关，因此很容易让人想用它评价处理器性能。这样的间接指标，<!-- source: p0739 -->已经误导过许多计算机设计者。评价存储层次结构时，也有类似诱惑：专注于未命中率，因为它同样与硬件速度无关。后面将看到，未命中率可能与指令数一样具有误导性。更好的指标是**平均存储器访问时间**：

$$
\text{平均存储器访问时间}=\text{命中时间}+\text{未命中率}\times\text{未命中代价}
$$

**命中时间**是在缓存中命中所需的时间，另外两项已经介绍过。各项可用绝对时间，例如命中需 0.25–1.0 ns，也可用处理器等待存储器的周期数，例如未命中代价为 150–200 周期。要记住，平均存储器访问时间仍是间接指标：虽然比未命中率好，却不能替代执行时间。

这个公式可以帮助我们在分离缓存与统一缓存之间选择。

### 例题：分离缓存还是统一缓存？

16 KiB 指令缓存加 16 KiB 数据缓存，与 32 KiB 统一缓存相比，哪种未命中率更低？使用图 B.6 的数据，假设 36% 的指令是数据传送。命中需 1 周期，未命中代价为 100 周期。统一缓存若只有一个端口，无法同时满足两次请求，因此加载或存储命中还需额外 1 周期；用第 3 章的流水线术语说，这产生了结构冒险。两种方案的平均存储器访问时间各是多少？假设均为带写缓冲区的直写缓存，忽略写缓冲区引起的停顿。

**解答。** 先把每 1000 条指令未命中次数转换为未命中率。由前面的通式可得：

$$
\text{未命中率}=\frac{\text{每 1000 条指令未命中次数}/1000}{\text{每条指令访存次数}}
$$

每条指令恰好需要一次取指，因此指令未命中率为：

$$
\text{未命中率}_{16\,\mathrm{KiB}\ \text{指令}}=\frac{3.82/1000}{1.00}=0.004
$$

36% 的指令是数据传送，因此数据未命中率为：

$$
\text{未命中率}_{16\,\mathrm{KiB}\ \text{数据}}=\frac{40.9/1000}{0.36}=0.114
$$

统一缓存要同时计入指令和数据访问：

$$
\text{未命中率}_{32\,\mathrm{KiB}\ \text{统一}}=\frac{43.3/1000}{1.00+0.36}=0.0318
$$

<!-- source: p0740 -->

前面指出，约 74% 的访存是取指。因此，分离缓存的总体未命中率为：

$$
74\%\times0.004+26\%\times0.114=0.0326
$$

所以，32 KiB 统一缓存的有效未命中率略低于两个 16 KiB 缓存。

平均存储器访问时间可以按指令、数据访问分别计算：

$$
\begin{aligned}
\text{平均存储器访问时间}
={}&\text{指令访问比例}\times(\text{命中时间}+\text{指令未命中率}\times\text{未命中代价})\\
&+\text{数据访问比例}\times(\text{命中时间}+\text{数据未命中率}\times\text{未命中代价})
\end{aligned}
$$

于是两种组织分别为：

$$
\begin{aligned}
\text{平均存储器访问时间}_{split}
&=74\%\times(1+0.004\times200)+26\%\times(1+0.114\times200)\\
&=74\%\times1.80+26\%\times23.80\\
&=1.332+6.188=7.52
\end{aligned}
$$

$$
\begin{aligned}
\text{平均存储器访问时间}_{unified}
&=74\%\times(1+0.0318\times200)+26\%\times(1+1+0.0318\times200)\\
&=74\%\times7.36+26\%\times8.36\\
&=5.446+2.174=7.62
\end{aligned}
$$

因此，本例的分离缓存每周期提供两个端口、避免结构冒险，尽管有效未命中率更高，平均访问时间却优于单端口统一缓存。[^split-penalty]

## 平均存储器访问时间与处理器性能

一个自然问题是：缓存未命中所决定的平均存储器访问时间，能否预测处理器性能？

首先，还有其他停顿来源，例如 I/O 设备使用存储器造成的争用。由于存储层次造成的停顿通常占主导，设计者经常假定所有存储器停顿都来自缓存未命中。这里也采用这一简化，但计算最终性能时，应计入所有存储器停顿。

其次，答案取决于处理器。对于顺序执行处理器（见第 3 章），基本可以：未命中会使处理器停顿，停顿时间与平均访问时间密切相关。暂时采用这个假设，下一小节再回到乱序处理器。

<!-- source: p0741 -->

如上一节所述，CPU 时间可建模为：

$$
\text{CPU 时间}=(\text{CPU 执行周期数}+\text{存储器停顿周期数})\times\text{时钟周期时间}
$$

这里会产生一个问题：缓存命中的周期，应归入 CPU 执行周期，还是存储器停顿周期？两种约定都说得通，但通常采用前者。

现在可以考察缓存对性能的影响。

### 例题：缓存如何影响顺序执行处理器

假设未命中代价为 200 周期；忽略存储器停顿时，每条指令通常需 1.0 周期；平均未命中率为 2%，每条指令平均访问存储器 1.5 次，即每 1000 条指令平均未命中 30 次。计入缓存行为后，性能受到什么影响？分别用每条指令未命中次数和未命中率计算。

**解答。**

$$
\text{CPU 时间}=IC\times\left(CPI_{execution}+\frac{\text{存储器停顿周期数}}{\text{指令数}}\right)\times\text{时钟周期时间}
$$

计入缓存未命中后：

$$
\begin{aligned}
\text{CPU 时间}_{with\ cache}
&=IC\times[1.0+(30/1000\times200)]\times\text{时钟周期时间}\\
&=IC\times7.00\times\text{时钟周期时间}
\end{aligned}
$$

改用未命中率计算：

$$
\text{CPU 时间}=IC\times\left(CPI_{execution}+\text{未命中率}\times\frac{\text{访存次数}}{\text{指令数}}\times\text{未命中代价}\right)\times\text{时钟周期时间}
$$

$$
\begin{aligned}
\text{CPU 时间}_{with\ cache}
&=IC\times[1.0+(1.5\times2\%\times200)]\times\text{时钟周期时间}\\
&=IC\times7.00\times\text{时钟周期时间}
\end{aligned}
$$

有无缓存停顿，时钟周期时间和指令数都相同。因此 CPU 时间增至七倍，CPI 从“完美缓存”的 1.00 变为可能未命中时的 7.00。如果完全没有存储层次结构，CPI 还会增加到 \(1.0+200\times1.5=301\)，执行时间超过有缓存系统的 40 倍！

<!-- source: p0742 -->

这个例子表明，缓存行为对性能的影响巨大。对于低 CPI、高频率处理器，未命中还有双重影响：

1. \(CPI_{execution}\) 越低，固定数量的未命中周期所占相对影响越大。
2. CPI 中的未命中代价以处理器周期计量。即使两台计算机的存储层次完全相同，频率更高的处理器，一次未命中也要经历更多周期，CPI 中存储器部分因而更大。

因此，低 CPI、高频率处理器更加依赖缓存；评价其性能时忽略缓存，也更加危险。Amdahl 定律再次发挥作用！

尽管最小化平均存储器访问时间是合理目标，本附录也经常这样做，但最终目标始终是缩短处理器执行时间。下例说明二者可能不同。

### 例题：命中速度、相联度与 CPU 时间

两种缓存组织对处理器性能有何影响？假设完美缓存下 CPI 为 1.0，时钟周期时间为 0.35 ns，每条指令访问存储器 1.4 次。两种缓存均为 128 KiB、64 字节块；一种直接映射，另一种二路组相联。

图 B.5 表明，组相联需要多路选择器，根据标签匹配结果选择组内的块。缓存命中速度可能直接决定处理器速度，因此假设为容纳该选择器，组相联处理器的时钟周期需延长到原来的 1.35 倍。初步近似认为，两种缓存未命中代价均为 65 ns；实际通常会取整到整数周期。

先计算平均访问时间，再计算处理器性能。命中时间均为 1 周期；128 KiB 直接映射缓存的未命中率为 2.1%，相同容量二路组相联为 1.9%。

**解答。**

$$
\text{平均存储器访问时间}=\text{命中时间}+\text{未命中率}\times\text{未命中代价}
$$

$$
\begin{aligned}
\text{平均存储器访问时间}_{1-way}&=0.35+0.021\times65=1.72\ \mathrm{ns}\\
\text{平均存储器访问时间}_{2-way}&=0.35\times1.35+0.019\times65=1.71\ \mathrm{ns}
\end{aligned}
$$

平均存储器访问时间以二路组相联更好。

<!-- source: p0743 -->

处理器性能为：

$$
\begin{aligned}
\text{CPU 时间}
&=IC\times\left(CPI_{execution}+\frac{\text{未命中次数}}{\text{指令数}}\times\text{未命中代价}\right)\times\text{时钟周期时间}\\
&=IC\times\left[CPI_{execution}\times\text{时钟周期时间}\right.\\
&\qquad\left.+\text{未命中率}\times\frac{\text{访存次数}}{\text{指令数}}\times\text{未命中代价}\times\text{时钟周期时间}\right]
\end{aligned}
$$

将“未命中代价 × 时钟周期时间”替换为 65 ns：

$$
\begin{aligned}
\text{CPU 时间}_{1-way}&=IC\times[1.0\times0.35+(0.021\times1.4\times65)]=2.26\times IC\\
\text{CPU 时间}_{2-way}&=IC\times[1.0\times0.35\times1.35+(0.019\times1.4\times65)]=2.20\times IC
\end{aligned}
$$

相对性能为：

$$
\frac{\text{CPU 时间}_{2-way}}{\text{CPU 时间}_{1-way}}
=\frac{2.26\times\text{指令数}}{2.20\times\text{指令数}}=1.03
$$

原书据此得出的结论是：与平均访问时间的比较不同，直接映射的平均性能略好。尽管二路组相联未命中较少，它却让所有指令的时钟周期都变长。由于 CPU 时间才是最终评价标准，而直接映射也更易实现，本例应选直接映射。[^direct-mapped-result]

## 未命中代价与乱序执行处理器

对于乱序处理器，应该怎样定义“未命中代价”？是访问存储器的完整未命中延迟，还是只有处理器必须停顿的那部分，即“暴露出来”、未能重叠的延迟？对于一直停顿到数据未命中完成的处理器，这个问题并不存在。

重新定义存储器停顿，将未命中代价改为不能重叠的延迟：

$$
\frac{\text{存储器停顿周期数}}{\text{指令数}}
=\frac{\text{未命中次数}}{\text{指令数}}\times(\text{总未命中延迟}-\text{重叠未命中延迟})
$$

类似地，某些乱序处理器会延长命中时间，性能公式中的相应部分也可以按总命中延迟减去重叠命中<!-- source: p0744 -->延迟来划分。还可以进一步区分无争用延迟与资源争用造成的延迟，把乱序处理器中的存储器资源争用纳入公式。这里只关注未命中延迟。

现在必须确定：

- **存储器延迟的长度**：乱序处理器中，访存操作从哪个时刻开始、到哪个时刻结束？
- **延迟重叠的长度**：与处理器执行的重叠从何时开始？等价地说，什么时候认为访存操作使处理器停顿？

乱序处理器很复杂，因此没有唯一正确的定义。

由于流水线退役阶段只看到已提交操作，可以把“某周期退役的指令数少于理论最大值”定义为处理器停顿，并将停顿归因于第一条无法退役的指令。这一定义并非万无一失。例如，优化某类停顿，不一定总能缩短执行时间，因为原来被它遮蔽的另一类停顿，可能随之暴露。

延迟计时可以从访存指令进入指令窗口、生成地址，或实际发往存储系统时开始。只要始终一致，哪一种都可以。

### 例题：隐藏部分未命中延迟

重新考虑前例，不过时钟周期较长的处理器现在支持乱序执行，仍采用直接映射缓存。假定 65 ns 未命中代价的 30% 可以重叠，也就是平均 CPU 存储器停顿时间降为 45.5 ns。

**解答。** 乱序（OOO）计算机的平均存储器访问时间为：

$$
\begin{aligned}
\text{平均存储器访问时间}_{1-way,OOO}
&=0.35\times1.35+0.021\times45.5\\
&=1.43\ \mathrm{ns}
\end{aligned}
$$

其性能为：

$$
\begin{aligned}
\text{CPU 时间}_{1-way,OOO}
&=IC\times[1.6\times0.35\times1.35+(0.021\times1.4\times45.5)]\\
&=2.09\times IC
\end{aligned}
$$

因此，尽管时钟周期明显更长，直接映射缓存的未命中率也更高，只要能隐藏 30% 的未命中代价，乱序计算机仍可略快一些。[^ooo-cpi]

总之，虽然为乱序处理器定义、测量存储器停顿的方法相当复杂，但必须注意这些问题，因为它们<!-- source: p0745 -->显著影响性能。复杂性在于，乱序处理器能容忍一部分缓存未命中延迟，而不损害性能。因此，评价存储层次的权衡时，设计者通常使用乱序处理器与存储器模拟器，确保降低平均存储延迟的优化，也确实改善程序性能。

图 B.7 汇总本附录的缓存公式，既总结本节，也便于查阅。

| 用途 | 公式 |
|---|---|
| 索引位数 | \(2^{\text{索引位数}}=\dfrac{\text{缓存容量}}{\text{块大小}\times\text{相联度}}\) |
| 执行时间 | \(\text{CPU 执行时间}=(\text{CPU 周期数}+\text{存储器停顿周期数})\times\text{时钟周期时间}\) |
| 存储器停顿 | \(\text{存储器停顿周期数}=\text{未命中次数}\times\text{未命中代价}\) |
| 按指令数表示停顿 | \(\text{存储器停顿周期数}=IC\times\dfrac{\text{未命中次数}}{\text{指令数}}\times\text{未命中代价}\) |
| 两种未命中指标 | \(\dfrac{\text{未命中次数}}{\text{指令数}}=\text{未命中率}\times\dfrac{\text{访存次数}}{\text{指令数}}\) |
| 平均存储器访问时间 | \(\text{平均存储器访问时间}=\text{命中时间}+\text{未命中率}\times\text{未命中代价}\) |
| 执行时间与每条指令停顿 | \(\text{CPU 执行时间}=IC\times\left(CPI_{execution}+\dfrac{\text{存储器停顿周期数}}{\text{指令数}}\right)\times\text{时钟周期时间}\) |
| 执行时间与每条指令未命中 | \(\text{CPU 执行时间}=IC\times\left(CPI_{execution}+\dfrac{\text{未命中次数}}{\text{指令数}}\times\text{未命中代价}\right)\times\text{时钟周期时间}\) |
| 执行时间与未命中率 | \(\text{CPU 执行时间}=IC\times\left(CPI_{execution}+\text{未命中率}\times\dfrac{\text{访存次数}}{\text{指令数}}\times\text{未命中代价}\right)\times\text{时钟周期时间}\) |
| 乱序执行中的停顿 | \(\dfrac{\text{存储器停顿周期数}}{\text{指令数}}=\dfrac{\text{未命中次数}}{\text{指令数}}\times(\text{总未命中延迟}-\text{重叠未命中延迟})\) |
| 两级缓存平均访问时间 | \(\text{平均存储器访问时间}=\text{命中时间}_{L1}+\text{未命中率}_{L1}\times(\text{命中时间}_{L2}+\text{未命中率}_{L2}\times\text{未命中代价}_{L2})\) |
| 两级缓存停顿 | \(\dfrac{\text{存储器停顿周期数}}{\text{指令数}}=\dfrac{\text{未命中次数}_{L1}}{\text{指令数}}\times\text{命中时间}_{L2}+\dfrac{\text{未命中次数}_{L2}}{\text{指令数}}\times\text{未命中代价}_{L2}\) |
{#fig-B.7 .wide .expand num="B.7" caption="本附录性能公式汇总。第一式计算缓存索引宽度，其余用于评价性能。最后两式涉及下一节前半部分介绍的多级缓存，也一并列入以便查阅。"}

[^split-penalty]: 译注：题干给出 100 周期，解答却代入 200 周期。
[^direct-mapped-result]: 译注：原书计算得到直接映射 2.26 IC、二路 2.20 IC，后面的比值却把标号与数字对调，并据此认定直接映射更快。按该页实际数值，2.20 IC 小于 2.26 IC，结论方向相反。
[^ooo-cpi]: 译注：本例公式中的 CPI 系数突然变为 1.6，前述条件是 1.0，题干未交代变化；原页确实如此，故保留并注明。原公式还把 CPU 拼作 CUP，译文统一为 CPU，数值不改。
