跳转到主要内容

B.2 缓存性能

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

平均存储器访问时间=命中时间+未命中率×未命中代价 \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 条指令未命中次数转换为未命中率。由前面的通式可得:

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

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

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

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

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

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

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

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

74%×0.004+26%×0.114=0.0326 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}

于是两种组织分别为:

平均存储器访问时间split=74%×(1+0.004×200)+26%×(1+0.114×200)=74%×1.80+26%×23.80=1.332+6.188=7.52 \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} 平均存储器访问时间unified=74%×(1+0.0318×200)+26%×(1+1+0.0318×200)=74%×7.36+26%×8.36=5.446+2.174=7.62 \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}

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

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

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

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

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

如上一节所述,CPU 时间可建模为:

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

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

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

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

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

解答。

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

计入缓存未命中后:

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

改用未命中率计算:

CPU 时间=IC×(CPIexecution+未命中率×访存次数指令数×未命中代价)×时钟周期时间 \text{CPU 时间}=IC\times\left(CPI_{execution}+\text{未命中率}\times\frac{\text{访存次数}}{\text{指令数}}\times\text{未命中代价}\right)\times\text{时钟周期时间} CPU 时间with cache=IC×[1.0+(1.5×2%×200)]×时钟周期时间=IC×7.00×时钟周期时间 \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×1.5=3011.0+200\times1.5=301,执行时间超过有缓存系统的 40 倍!

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

  1. CPIexecutionCPI_{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{未命中代价} 平均存储器访问时间1−way=0.35+0.021×65=1.72 ns平均存储器访问时间2−way=0.35×1.35+0.019×65=1.71 ns \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}

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

处理器性能为:

CPU 时间=IC×(CPIexecution+未命中次数指令数×未命中代价)×时钟周期时间=IC×[CPIexecution×时钟周期时间+未命中率×访存次数指令数×未命中代价×时钟周期时间] \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:

CPU 时间1−way=IC×[1.0×0.35+(0.021×1.4×65)]=2.26×ICCPU 时间2−way=IC×[1.0×0.35×1.35+(0.019×1.4×65)]=2.20×IC \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}

相对性能为:

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

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

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

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

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

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

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

现在必须确定:

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

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

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

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

例题:隐藏部分未命中延迟

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

解答。 乱序(OOO)计算机的平均存储器访问时间为:

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

其性能为:

CPU 时间1−way,OOO=IC×[1.6×0.35×1.35+(0.021×1.4×45.5)]=2.09×IC \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% 的未命中代价,乱序计算机仍可略快一些。3

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

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

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

  1. 译注:题干给出 100 周期,解答却代入 200 周期。 ↩︎

  2. 译注:原书计算得到直接映射 2.26 IC、二路 2.20 IC,后面的比值却把标号与数字对调,并据此认定直接映射更快。按该页实际数值,2.20 IC 小于 2.26 IC,结论方向相反。 ↩︎

  3. 译注:本例公式中的 CPI 系数突然变为 1.6,前述条件是 1.0,题干未交代变化;原页确实如此,故保留并注明。原公式还把 CPU 拼作 CUP,译文统一为 CPU,数值不改。 ↩︎

表格阅读