我正在阅读Norman P. Jouppi 的Cache Write Policies论文,我理解为什么 write-invalidate(在第 193 页定义)适用于直接映射缓存,这是因为能够写入检查标签的数据,如果发现如果未命中,则缓存行无效,因为它已被写入损坏。这可以在一个周期内完成。但是,如果将 write-invalidate 用于 set-associative 缓存有什么好处吗?实际处理器中用于 L1 缓存的通常配置是什么?他们是否使用直接或设置关联和写验证/写周围/写无效/写时获取策略?
1 回答
TL:DR:对于使用 write-invalidate 的非阻塞缓存,将其从直接映射更改为 set-associative 可能会降低命中率,除非写入非常罕见,或者意味着您引入了需要阻塞的可能性。
Write-invalidate 仅对带有简单缓存的简单有序管道有意义,即使没有存储缓冲区,也会尝试避免停止管道,并且以命中率为代价运行得非常快。如果您要进行更改以提高命中率,那么首先要做的事情之一就是摆脱 write-invalidate(通常是 write-back + write-allocate + fetch-on-write)。通过一些丑陋的权衡,可以使用 set-associative cache 进行写入无效,但您不会喜欢结果。
您链接的 1993 年论文使用该术语来表示现代缓存一致性机制含义以外的其他含义。在论文中:
write-before-hit、no-fetch-on-write 和 no-write-allocate 的组合我们称之为 write-invalidate
是的,如今现实世界的缓存基本上都是集合关联的;更复杂的标签比较器逻辑值得增加相同数据大小的命中率。 英特尔酷睿 i7 处理器中使用了哪种缓存映射技术?有一些通用的东西,而不仅仅是x86。直接映射缓存的现代示例包括 DRAM 缓存,当英特尔平台上的一部分持久内存在内存模式下运行时。来自多个供应商的许多服务器级处理器也支持 L3 方式分区,因此您可以例如为基本上表现为直接映射缓存的线程分配一种方式。
对于现代 CPU 缓存,写策略通常是 write-allocate + fetch-on-write + no-write-before-hit;一些 ISA 提供诸如特殊指令之类的方法来绕过不会很快重新读取的“非临时”存储的缓存,以避免这些情况下的缓存污染。大多数工作负载确实以足够的时间局部性重新加载其存储,因此写入分配是唯一明智的选择,尤其是当缓存更大和/或关联性更强时,它们更有可能挂在一条线上直到下一次读取或写。
在同一行进行多次小写入也很常见,这使得 write-allocate非常有价值,尤其是在存储缓冲区无法合并这些写入的情况下。
但是,如果将 write-invalidate 用于 set-associative 缓存有什么好处吗?
似乎并非如此。
它的唯一优点是不会停止缺少存储缓冲区(论文中的“写缓冲区”)的简单有序管道。它允许与标签检查并行写入,因此您可以在修改该行后找出是否命中。(现代 CPU确实使用存储缓冲区将存储提交到 L1d 与存储执行分离并隐藏存储未命中延迟。即使是有序 CPU 通常也具有存储缓冲区以允许 RFO 的内存级并行性(为所有权而读取)。 (例如手机中的 ARM Cortex-A53)。
无论如何,在集合关联缓存中,您需要检查标签以了解在写入命中时要写入集合的哪个“方式”。 (或者检测一个未命中并根据某些策略选择一个来驱逐,例如使用一些额外状态位的随机或伪 LRU,或者如果没有写入分配,则回写)。 如果您等到标签检查之后才找到写入方式,那么您就失去了 write-invalidate 的唯一好处。
盲目地以随机方式写入可能会导致命中方式与您猜测的方式不同的情况。方式预测是一回事(并且比随机预测做得更好),但对这样的写入进行错误预测的缺点是不必要地使一行无效,而不仅仅是一点额外的延迟。 现代缓存中的方式预测。我不知道成功率方式预测通常会达到什么样的效果。我猜不是很好,最多可能是 80% 到 90%。可能花费晶体管来做路预测会更好地花在其他地方,做一些比写无效更糟糕的事情!带有存储转发的存储缓冲区可能成本更高,但要好得多。
write-invalidate 的优点是有助于使缓存非阻塞。但是,如果您确实以不同于您选择的方式发现写入命中时需要纠正这种情况,您需要返回并纠正这种情况,更新正确的行。所以你会失去非阻塞属性。 从不停止比通常不停止要好,因为这意味着您甚至根本不需要让硬件处理这种可能的情况。(尽管您确实需要能够暂停内存。)
以所有方式写入可以避免一种方式写入另一种情况。但是最多只有一次命中,其余的必须无效。对命中率的负面影响将随着关联性显着增加。(除非写入与读取相比非常罕见,否则降低关联性可能有助于使用 write-all-ways 策略的命中率,因此对于给定的总缓存容量,如果您坚持完全非直接映射,则直接映射可能是最佳选择-blocking write-invalidate。)即使对于直接映射缓存,论文本身给出的实验评估表明,与其他评估的写入策略相比,write-invalidate 具有更高的未命中率。因此,只有在减少延迟和带宽需求的好处超过了高丢失率的损害时,它才是赢家。
此外,正如我所说,write-allocate 对 CPU 非常有用,尤其是当它是 set-associative 时,因此您要花费更多资源来获得更高的命中率。您可能仍然可以通过在未命中时触发 fetch 来实现 write-allocate,记住您存储数据的行中的位置,并在它到达时将其与行的旧副本合并。
你不想通过吹走不需要死的线来打败它。
此外,write-invalidate 意味着即使对于 write hits 也是直写,因为如果一行脏了,它可能会丢失数据。 但是回写在现代 L1d 缓存中也非常好,可以将较大/较慢的缓存与写入带宽隔离开来。(特别是如果没有每核专用 L2 来单独减少共享缓存的总流量。)但是,AMD Bulldozer 系列确实有一个直写 L1d,它和一个回写 L2 之间有一个小的 4k 写缓冲区。这通常被认为是失败的实验或设计的弱点,他们放弃了它,转而支持 Zen 的标准回写写分配 L1d。 当对 pages 使用 write-through 缓存策略时。
总而言之,write-invalidate 与现代主流 CPU 设计已确定为最佳选择的几件事不兼容,你会在大多数主流 CPU 设计中找到
- 写分配写策略
- 回写(不是直写)。 https://en.wikipedia.org/wiki/Cache_(computing)#Writing_policies
- 集合关联(只能通过方式预测部分缓解的巨大缺点)
- 存储缓冲区以将存储未命中与执行分离,并允许内存并行性。(并非严格不兼容,但存储缓冲区使其毫无意义。对于 OoO exec 是必需的,并且广泛用于 in-order)
缓存一致 SMP 系统中的写无效
你永远不会考虑在单芯片多核 CPU 中使用它;在开始构建更多内核之前,在每个内核上花费更多晶体管以获得更多唾手可得的果实。例如适当的存储缓冲区。如果您想为多个停顿很多的低 IPC 线程提供高吞吐量,请使用某种 SMT 风格。
但是对于多插槽 SMP,如果您想使用多个您可以构建的最大单核芯片,这在历史上可能是有意义的,而且这仍然不够大,无法仅使用存储缓冲区来代替这个。
我想在一个仍然相当快的私有中型回写集关联 L2 前面使用一个真正“薄”的直接映射直写 L1d 甚至是有意义的。(也许将其称为 L0d 缓存,因为它可以像无序存储缓冲区一样工作。下一级缓存仍会从这个小型直接映射缓存的低命中率中看到大量读取和写入。)
通常所有缓存(包括 L1d)都是同一个全局一致性域的一部分,因此在您拥有独占所有权之前不会写入 L1d 缓存。(作为标签检查的一部分进行检查。)但是如果这个 L1d / L0d 不是那样的,那么它就不是连贯的,更像是一个存储缓冲区。
当然,您需要将 L2 的直写排入队列,并在无法跟上时最终停止,因此您只是增加了复杂性。写入 L2 机制还需要处理等待 L2 在写入之前获得行的独占所有权(MESI Exclusive 或 Modified 状态)。所以这只是一个无序的存储缓冲区。
写入尚未到达 L2 的行的情况很有趣:如果它是 L0d 写入命中,您实际上可以免费获得存储合并。为此,您需要按字或按字节需要写回位(又名脏位)。通常直写将在行内的偏移量仍然可用时沿写入发送,但如果 L2 还没有准备好接受它(例如,由于写入未命中),那么您就不能这样做。这是将其变形为写入组合缓冲区。将整行标记为需要回写不起作用,因为未写入的部分仍然无效。
但是,如果它是在仍未完成回写到 L2 的行上的写未命中(相同的缓存行,不同的标记位),那么您将遇到一个大问题,因为您将使仍然“脏”的行(具有一些旧商店数据的唯一副本)。在写作之前您无法检测到这一点;重点是与检查标签并行编写。
可能仍然可以使这项工作:如果缓存访问是读+写交换,将先前的值保存在一个字缓冲区中(或任何最大写入大小),您仍然拥有所有数据。停止所有内容(包括此行的写回,以免错误数据在一致的 L2 缓存中全局可见)。然后交换回来,等待该 L0d 行的旧状态实际写回该地址,然后将 tmp 缓冲区存储到 L0d 并更新标记和需要写回位以反映此存储。因此,附近商店之间的混叠变得更加昂贵并阻碍了管道。或者也许你可以让非内存指令继续执行,只在下一个停止执行加载或存储。(如果您有足够的晶体管预算来避免大部分失速,您可能可以使用完全不同的策略,例如拥有一个存储缓冲区和一个正常的 L1d。)
为了可用(假设您解决了dirty-store-miss 问题),您需要一些方法来跟踪存储(和加载)的相对顺序。如果这就像确保整个 L0d 中的每个条目在允许另一个写入之前完成其写入过程一样简单,那么即使存储存储屏障也会非常昂贵。CPU 执行的订单跟踪越少,障碍就必须越昂贵(冲洗更多的东西以确保)。