如果我有一些原子变量
- 访问相对较少(低争用)
- 由线程/核心统一随机访问(即,如果线程 A 写入变量,则很可能不是A下一个访问该变量)
那么,当 A 完成对变量的写入时,我是否应该以某种方式显式地将变量从线程 A 核心的 l1/l2/l3 缓存中刷新出来,以便当其他线程 B 需要访问该变量时后来,它在 RAM 中找到一个干净的缓存行,而不是另一个核心拥有的脏缓存行?
一些子问题
- 从 RAM 访问干净的缓存行比从另一个核心访问脏的缓存行更快还是更慢?我假设前者在这里更快,但也许不是?
- 而不是一直到 RAM,刷新到共享 l3 缓存(并且从每个核心的 l1/l2 中)是否足够?
- 如果将缓存行刷新/写入 RAM(或 l3)是有益的,我该怎么做?
另外,我应该阅读哪些涵盖此类信息的文档/等?
TL:DR:在 x86 上你想要
cldemote
. 其他事情可能不值得做,特别是如果您的编写器线程可以在此商店之后做有用的工作。或者,如果没有,并且操作系统没有其他线程在该核心上运行,则将核心置于深度睡眠状态将涉及 CPU 在关闭其专用缓存之前写回其脏缓存行。我预计读取 RAM 通常比另一个内核中的脏线慢,尤其是在单插槽系统中。 (在多插槽 NUMA 系统中,如果远程核心具有由本地 DRAM 支持的缓存行的脏副本,则可能会改变情况,或者至少使 DRAM 不那么落后。)
如果不存在好的(对作者来说便宜的)回写指令,那么什么也不做可能比走得太远更好。
DRAM 延迟首先必须在 L3 中丢失,然后来自该 L3 切片的消息必须通过互连(例如环形总线或网格)到达内存控制器,然后您必须等待外部内存总线上的 DRAM 命令延迟。并且内存控制器中可能已经有一些排队的请求,您的内存控制器在后面等待。然后数据必须返回到执行加载的核心。
另一个核心中的脏线在检测到 L3 未命中后还会涉及另一条消息,但会发送到拥有该线的核心。(L3 标签本身可能表明这一点,就像在 Intel CPU 上一样,出于这个原因,它使用包容性 L3。否则,一个单独的目录充当窥探过滤器)。它应该能够比 DRAM 控制器更快地响应,因为它只需从快速 L2 或 L1d 缓存中读取数据并将其发送到 L3 切片。(而且还直接到达想要它的核心?)
理想的情况是最后一级(通常是 L3)缓存命中,以支持一致性流量。 因此,您希望从最后一个核心的私有 L1d/L2 缓存中逐出该行来写入它。(或者至少写回 L3 并在这些私有缓存中降级为共享状态,而不是独占/修改。因此,从同一核心的读取可能会在 L1d 中命中,只需要离核流量(并且 RFO = 读取所有权),如果它又写了。)
但并非所有 ISA 都有指令可以廉价地执行此操作(不会使写入器减慢太多)或不会走得太远并强制写入 RAM。 像 x86 这样的指令
clwb
强制写入 RAM,但之后在 L3 中保持线路干净,在某些用例中可能值得考虑,但会浪费 DRAM 带宽。(但请注意,Skylake 实现clwb
为clflushopt
;仅在 Ice Lake 中以及后来的版本中,它实际上保留数据缓存以及写回 DRAM)。如果不经常访问它,有时它会在任何其他核心读取或写入它之前,仅从最后一个要写入的核心上的普通活动(例如循环数组)写回 L3。这太好了,任何从 L3 强制驱逐的行为都可以防止这种情况发生。如果该行的访问频率足以在 L3 缓存中正常保持热状态,那么您就不想破坏它。
如果写入器线程/核心在写入后没有任何其他事情可做,您可以想象访问其他缓存行以尝试通过正常的伪 LRU 机制驱逐重要的写入,但这只有在负载下一个读取器的延迟非常重要,以至于值得在写入线程中浪费大量 CPU 时间并为其他缓存行生成额外的一致性流量,以便在其他线程中针对稍后的时间进行优化。
有关的:
有没有办法编写Intel CPU直接核对核通信代码?- CPU 对于在一个核心上写入、在另一个核心上读取进行了很好的优化,因为这是实际代码中的常见模式。
除了提供必要的保证之外,硬件内存屏障是否还能使原子操作的可见性更快?(即,当该核心提交到 L1d 时,或者当它使其他缓存无效时,它们必须向此请求:不,它不会直接使速度更快,并且不值得这样做。)
x86 MESI 无效缓存行延迟问题- 建议让第三个线程每毫秒读取共享数据,以从最后一个写入器中提取数据,从而使高优先级线程最终读取数据的效率更高。
CPU 缓存抑制(相当以 x86 为中心)
各种 ISA 的说明
用于将脏缓存行写入下一级缓存的 RISC-V 指令- RISC-V 不存在这样的指令,至少在 2020 年不存在。
ARM/AArch64:我不知道,但如果有的话我不会感到惊讶。欢迎编辑。
还有其他 ISA 具有有趣的缓存管理指令吗?
x86 - 直到最近才发现什么好东西
cldemote
NT 存储:绕过高速缓存一直到 L3,并强制逐出该行(如果以前不是)。所以这是一场灾难。
clflush
/clflushopt
- 这些会一直驱逐到 DRAM,您不希望这样。(与缓存预取提示相反的是刷新小数组的一些性能数据。)clwb
- 此操作会一直写回 DRAM,但会将数据缓存在 Ice Lake 及更高版本上。(在 Skylake/Cascade Lake 中,它实际上与 运行相同clflushopt
。至少它运行时没有错误,因此未来的持久内存库可以直接使用它,而无需检查 ISA 版本内容。)并且对 DRAM 的提交(可能是 NV-DIMM)可以被排序sfence
,所以大概核心必须一直跟踪它,占用队列中的空间?cldemote
在 Tremont 和 Sapphire Rapids 中 - 正是为这个用例而设计的:它是一个性能提示,就像预取的反面一样。它写回到L3。nop
它在不支持它的 CPU 上运行,因为他们故意选择了现有 CPU 已经作为以前未记录的 NOP 运行的编码。与 不同的是
clwb
,它没有保证的行为(只是提示),也没有排序。即使是栅栏指令,也仅是wrt。存储到同一缓存行。因此,在通过互连发送带有要写入 L3 的数据的消息后,核心不必跟踪请求(并通知该核心的线路副本不干净。)