L1 缓存控制器处理来自 CPU 的内存请求的顺序

The ordering of L1 cache controller to process memory requests from CPU

在总存储顺序 (TSO) 内存一致性模型下,x86 cpu 将有一个写入缓冲区来缓冲写入请求,并可以从写入缓冲区为重新排序的读取请求提供服务。并且它说写缓冲区中的写请求将退出并以FIFO顺序向缓存层次结构发出,这与程序顺序相同。

我很好奇:

为了服务从写缓冲区发出的写请求,一级缓存控制器是否处理写请求,完成写请求的缓存一致性,并按照与发出顺序相同的顺序将数据插入一级缓存?

你的术语很不寻常。你说“完成缓存一致性”;实际发生的是核心必须在 可以修改它之前 获得缓存行的(独占)所有权。在 instant/cycle 发生修改时,它成为缓存一致性协议中所有参与者共享的内存内容视图的一部分。

所以,是的,您确实“完成了缓存一致性”= 获得独占所有权 商店甚至可以进入缓存并变得全局可见 = 可用于共享该缓存行的请求.缓存始终保持一致性(这是 MESI 的重点),而不是不同步然后等待一致性。我认为你的困惑源于你的心理模型与现实相匹配。

(弱序架构具有令人费解的可能性,例如并非所有核心都以相同的顺序从其他两个核心看到存储;这可能会在 之前发生。)


我想你知道一些,但让我从基础开始。

每个内核中的 L1 缓存参与缓存一致性协议,该协议使其缓存与一致性域中的其他缓存保持一致(例如 L2 和 L3,以及其他内核中的 L1,但不是视频 RAM 缓存内部) GPU).

加载在从 L1 缓存中读取数据时变得全局可见 ( or from uncacheable RAM or MMIO). MFENCE can force them to wait for earlier stores to become globally visible before sampling L1, to avoid StoreLoad reordering.

存储在其数据提交到 L1 缓存的那一刻就变得全局可见。发生这种情况所需的条件是:

  • 执行完毕:数据+地址在存储缓冲区条目中。 (即一旦输入准备就绪,存储地址和存储数据 uops 在适当的端口上执行,将地址和数据写入存储缓冲区,也就是 Intel CPU 上的内存顺序缓冲区)。

  • 它是 retired from the out-of-order part of the core, and thus known to be non-speculative. Before retirement, we don't know that it and all preceding instructions won't fault,或者它不在分支预测错误或其他错误推测的阴影下。

    退休只能在它完成执行后发生,但与对 L1d 的承诺无关。存储缓冲区可以继续跟踪非推测性存储,即使在 ROB(乱序执行重新排序缓冲区)忘记了存储 指令.

  • 前面的所有 loads/stores/fences 已经全局可见(因为 x86 的内存排序规则)。这不包括弱排序操作(NT 存储);其他loads/stores可以通过。

  • 缓存行处于MESI/MESIF/MOESI缓存一致性协议的Exclusive或Modified状态,在当前核心的L1d缓存中。 如果 RFO(读取所有权)在缓存的外部级别遇到缓存未命中,或者与其他也希望独占访问写入或原子 RMW 缓存行的内核争用,这可能需要很长时间。

有关允许的状态转换图和详细信息,请参阅维基百科的 MESI article。关键点是一致性是通过只允许核心修改它的缓存行副本来实现的,当它确定没有其他缓存包含该行时,所以两个冲突的副本是不可能的存在同一行。

Intel CPU 实际上使用 MESIF, while AMD CPUs actually use MOESI 允许脏数据的缓存-> 缓存数据传输,而不是像基本 MESI 协议要求的那样写回共享外部缓存。

另请注意,现代英特尔设计(在 Skylake-AVX512 之前)实现使用 large shared inclusive L3 cache as a backstop for cache-coherency,因此探听请求实际上不必广播到所有内核;他们只是检查 L3 标签(其中包含额外的元数据来跟踪哪个内核正在缓存什么。
Intel 的 L3 是包含标签的,即使对于内部高速缓存处于排他或修改状态且因此在 L3 中无效的行也是如此。参见 this paper for more details of a simplified version of what Intel does)。

也相关:I wrote an answer recently about why we have small/fast L1 + larger L2/L3, instead of one big cache,包括一些指向其他缓存相关内容的链接。


回到正题:

是的,存储按程序顺序提交到 L1 ,因为这是 x86 要求它们变得全局可见的顺序。 L1 提交顺序与全局可见性顺序相同。

而不是“完成缓存一致性”,而应该说“获得缓存行的所有权”。这涉及使用缓存一致性协议与其他缓存进行通信,所以我猜你的意思可能是“使用缓存一致性协议完成获得独占所有权”。

MESI wiki 文章的 memory ordering 部分指出,存储队列中的缓冲存储与一般的乱序执行是分开的。

存储缓冲区将对 L1d 的提交与 OoO exec 退休分离。这可能会隐藏 lot 比常规无序 window 大小更多的存储延迟。然而,退役存储 必须 最终发生(以正确的顺序),即使中断到达,因此允许大量退役但未提交的存储会增加中断延迟。

存储缓冲区尝试尽快将退役的存储提交给 L1d,但它受到内存排序规则的限制。 (即其他核心很快就会看到存储;您不需要栅栏来刷新存储缓冲区,除非您需要当前线程等待它发生,然后再在此线程中加载。例如,对于顺序一致的存储。)

在弱排序的 ISA 上,较晚的存储可以提交到 L1d,而较早的存储仍在等待缓存未命中。 (但是您仍然需要一个内存顺序缓冲区来保留程序顺序中单核 运行 指令的错觉。)

存储缓冲区可以同时有多个缓存未命中,因为即使在强顺序 x86 上,它也可以在该存储是缓冲区中最旧的缓存行之前发送一个缓存行的 RFO。

是的 在像 x86-TSO 这样的模型中,存储很可能按程序顺序提交给 L1,并且 很好地涵盖了它。也就是说,存储缓冲区按程序顺序维护,在继续之前,核心只会将最旧的存储(或者可能是几个连续的最旧存储,如果它们都进入同一缓存行)提交到 L1。 1

但是,您在评论中提到您担心这可能会影响性能,因为这实际上会使存储缓冲区提交一个阻塞(序列化)进程:

And why I am confused about this problem is that cache controller could handle the requests in a non-blocking way. But, to conform to the TSO and make sure data globally visible on a multi-core system, should cache controller follow the store ordering? Because if there are two variable A and B being updated sequentially on core 1 and core 2 get the updated B from core 1, then core 2 must also can see the updated A. And to achieve this, I think the private cache hierarchy on core 1 have to finishes the cache coherence of the variable A and B in order and make them globally visible. Am I right?

好消息是,即使存储缓冲区可能仅以有序方式将最旧的存储提交到 L1,它仍然可以通过在存储中向前看,相对于内存子系统的其余部分获得足够的并行性缓冲区并发出 prefetch RFO 请求:尝试在本地核心中获取处于 E 状态的行,甚至在存储首先提交到 L1 之前。

这种方法不违反顺序,因为存储仍然按程序顺序编写,但它在解决 L1 存储未命中时允许完全并行。无论如何,真正重要的是 L1 存储未命中:L1 中的存储命中可以快速提交,每个周期至少 1 个,因此提交一堆命中并没有多大帮助:但是在存储未命中上获得 MLP 非常重要,尤其是对于分散的存储预取器无法处理。

x86 芯片真的使用这样的技术吗?几乎可以确定。最令人信服的是,对一长串随机写入的测试显示平均延迟比完整内存延迟要好得多,这意味着 MLP 明显优于一个。您还可以找到像 this one or this one 这样的专利,其中英特尔几乎完全描述了这种方法。

不过,没有什么是完美的。有一些证据表明,当商店缺少 L1 时,订购问题会导致 weird performance hiccups,即使它们进入了 L2。


1 当然 可能 如果保持 错觉 [=] 它可以乱序提交存储38=] 的顺序提交,例如,在恢复顺序之前不放弃乱序写入的缓存行的所有权,但这很容易出现死锁和其他复杂情况,我没有证据表明 x86 会这样做。