Intel-x86:WC、WB和UC Memory的交互

问题描述

我不清楚 x86 架构上不同内存区域的内存排序保证。具体来说,Intel 手册指出 WC、WB 和 UC 遵循不同的内存顺序,如下所示。

WC:弱排序(例如可以重新排序位于不同位置的两个商店)

WB(以及 WT 和 WP,即所有可缓存的内存类型):处理器排序(也称为 TSO,可以在不同位置的旧存储之前对较新的负载进行重新排序)

UC:强排序(所有指令按程序顺序执行,不能重新排序)

我不清楚的是 UC 和其他地区之间的互动。具体来说,手册提到:

(A) UC 访问是强有序的,因为它们总是按程序顺序执行,不能重新排序;和

(B) WC 访问是弱排序的,因此可以重新排序。

因此,在 (A) 和 (B) 之间,尚不清楚 UC 访问和 WC/WB 访问是如何排序的。彼此。

1a) [UC-store/WC-store 排序] 例如,让我们假设 x 在 UC 内存中,y 是 WC 内存。那么在下面的多线程程序中,是否可以从y加载1和从x加载0?如果可以重新排序线程 0 中的两个存储,这将是可能的。 (我在两个负载之间放置了一个 mfence,希望它能阻止负载被重新排序,因为我不清楚 WC/UC 负载是否可以重新排序;参见下面的 3a)

       thread 0       |   thread 1
     store [x] <-- 1  |   load [y]; mfence 
     store [y] <-- 1  |   load [x]

1b) 如果相反(对称地)x 在 WC 内存中而 y 在 UC 内存中呢?

2a) [UC-store/WB-load 排序] 同样,UC-store 和 WB-load(在不同位置)可以重新排序吗?让我们假设 x 在 UC 内存中,z 在 WB 内存中。那么在下面的多线程程序中,有没有可能两个load都加载0呢?如果由于存储缓冲而 x 和 z 都在 WB emory 中,这将是可能的(或者可以证明:每个线程中的新负载可以在旧存储之前重新排序,因为它们位于不同的位置)。但由于对 x 的访问是在 UC 内存中,因此尚不清楚这种行为是否可能。

       thread 0       |   thread 1
     store [x] <-- 1  |   store [z] <-- 1 
     load [z]         |   load [x]

2b) [UC-store/WC-load 排序] 如果 z 在 WC 内存中(而 x 在 UC 内存中)怎么办?那么两个负载都可以加载 0 吗?

3a) [UC-load/WC-load 排序] UC-load 和 WC-load 可以重新排序吗?再一次,让我们假设 x 在 UC 内存中,y 在 WC 内存中。那么,在下面的多线程程序中,是否可以从 y 加载 1,从 x 加载 0?如果可以重新排序两个负载,这将是可能的(我相信这两个商店由于中间的 sfence 而无法重新排序;根据 1a 的答案,可能不需要 sfence)。>

       thread 0               |   thread 1
     store [x] <-- 1; sfence  |   load [y] 
     store [y] <-- 1          |   load [x]

3b) 如果相反(对称地)x 在 WC 内存中而 y 在 UC 内存中呢?

4a) [WB-load/WC-load 排序] 如果在上面 3a 的例子中,x 在 WB 内存中(而不是 UC),y 在 WC 内存中(和以前一样)怎么办?

4b) 如果(对称地)x 在 WC 内存中而 y 在 WB 内存中呢?

解决方法

Intel 对 UC 内存类型的描述在手册的第 3 卷中分布在多个地方。我将重点介绍与内存排序相关的部分。主要内容来自第 8.2.5 节:

强未缓存 (UC) 内存类型强制对内存访问使用强排序模型。这里,所有对UC内存的读写 区域出现在总线上,并且乱序或推测性访问是 未执行。

这表明可以保证按程序顺序观察跨不同指令的 UC 内存访问。类似的声明出现在第 11.3 节中。两者都没有说明 UC 和其他内存类型之间的排序。这里需要注意的是,由于所有 UC 访问的全局可观察性是有序的,因此不可能发生从 UC 商店到 UC 负载的商店转发。此外,UC 存储不会在 WCB 中合并或组合,尽管它们确实会通过这些缓冲区,因为这是从核心到非核心的所有请求都必须经过的物理路径。

以下两个引文讨论了 UC 加载和存储以及任何类型的先前或以后存储之间的排序保证。重点是我的。

第 11.3 节:

如果WC缓冲区被部分填满,写入可能会延迟到 序列化事件的下一次发生;例如 SFENCE 或 MFENCE 指令、CPUID 或其他序列化指令、读取或 写入未缓存的内存、中断发生或执行 一条 LOCK 指令(包括带有 XACQUIRE 或 XRELEASE 前缀).

这意味着 UC 访问是根据较早的 WC 存储排序的。将此与 WB 访问进行对比,后者不与较早的 WC 存储一起订购,因为它们 WB 访问不会导致 WCB 被耗尽。

第 22.34 节:

存储在存储缓冲区中的写入总是写入到内存中 程序顺序,“快速字符串”存储操作除外 (请参阅第 8.2.4 节,“快速字符串操作和无序存储”)。

这意味着存储总是按照程序顺序从存储缓冲区提交,这意味着所有类型的存储,除了 WC,跨不同指令的存储都是按程序顺序观察的。任何类型的商店都不能与较早的 UC 商店重新排序。

英特尔不保证非 UC 加载的排序与较早或较晚的 UC 访问(加载或存储),因此排序在架构上是可能的。

AMD 内存模型针对所有内存类型进行了更准确的描述。它明确指出,可以使用较早的 UC 存储重新排序非 UC 负载,并且可以使用较早的 UC 负载重新排序 WC/WC+ 负载。到目前为止,Intel 和 AMD 模型彼此一致。但是,AMD 模型还指出,UC 负载不能通过任何类型的早期负载。据我所知,英特尔在手册中的任何地方都没有说明这一点。

关于示例 4a 和 4b,英特尔不保证 WB 加载和 WC 加载之间的顺序。 AMD 模型允许 WC 负载通过较早的 WB 负载,但不能相反。

,

警告:我在所有这些中都忽略了缓存一致性;因为它使一切变得复杂,并且对理解 WB、WT、WP、WC 或 WC 的工作方式或任何答案没有任何影响。

假设您有 4 块,例如:

          ________
         |        |
         | Caches |
         |________|
         /       \
  ______/_       _\__________________
 |        |     |                    |
 |  CPU   |-----|  Physical address  |
 |  core  |     |  space (e.g. RAM)  |
 |________|     |____________________|
        \        /
       __\______/_
      |           |
      | Write     |
      | combining |
      | buffer    |
      |___________|

就CPU的核心而言;一切总是“处理器订购”(带有商店转发的总商店订购)。 WC、WB、WT、WP 和 UC 之间的唯一区别是数据在 CPU 内核和物理地址空间之间的路径。

对于 UC,写入直接进入物理地址空间,读取直接来自物理地址空间。

对于 WC,写入进入“写入组合缓冲区”,在那里它们与之前的写入组合并最终从缓冲区中驱逐(稍后发送到物理地址空间)。 WC 的读取直接来自物理地址空间。

对于 WB,写入进入缓存,稍后从缓存中驱逐(并发送到物理地址空间)。对于 WT 写入,同时进入缓存和物理地址空间。对于 WP 写入被丢弃并且根本不会到达物理地址空间。对于所有这些,读取来自缓存(并导致在“缓存未命中”时从物理地址空间获取到缓存)。

还有其他 3 件事会影响这一点:

  • 商店转发。任何存储都可以转发到“CPU 核心”内的稍后加载,无论该区域应该是 WC、WB、WT 还是 UC。这意味着声称 80x86 具有“总商店订购量”在技术上是错误的。

  • 非临时存储会导致数据进入写入组合缓冲区(无论存储区最初是 WB 或 WT 还是 ... 或 UC)。非临时读取允许在较早的存储之前进行稍后的非临时读取。

  • 写栅栏阻止存储转发并等待写合并缓冲区被清空。读取栅栏导致 CPU 等待,直到较早的读取完成,然后才允许稍后的读取。 mfence 指令结合了读栅栏和写栅栏的行为。 注意:我失去了对 lfence 的跟踪 - 对于某些/最近的 CPU,我认为它变成了黑客以帮助缓解“幽灵”安全问题(我认为它变成了一个推测执行障碍,而不仅仅是一个读取栅栏).

现在...

1a)

  thread 0             |     thread 1
store [x_in_UC] <-- 1  |   load [y_in_WC]; mfence 
store [y_in_WC] <-- 1  |   load [x_in_UC]

在这种情况下,mfence 无关紧要(之前的 load [y_in_WC] 无论如何都像 UC);但是到 y_in_WC 的存储可能需要很长时间才能到达物理地址空间(这并不重要,因为它可能是最后的)。不可能从 y 加载 1,从 x 加载 0。

1b)

   thread 0             |     thread 1
 store [x_in_WC] <-- 1  |   load [y_in_UC]; mfence 
 store [y_in_UC] <-- 1  |   load [x_in_WC]

在这种情况下,store [x_in_WC] 可能需要很长时间才能到达物理地址空间;这意味着 load [x_in_WC] 加载的数据可能会从物理地址空间中获取较旧的数据(即使加载是在存储之后完成的)。很可能从 y 加载 1,从 x 加载 0。

2a) 线程 0 |线程 1 存储 [x_in_UC]

在这种情况下,根本没有什么可混淆的(一切都按照程序顺序发生;只是 store [z_in_WB] 写入缓存而 load [z_in_WB] 从缓存读取);并且不可能两个加载都加载 0。注意:外部观察者(例如观察物理地址空间的设备)可能很长时间看不到存储到 z_in_WB

2b)

   thread 0             |     thread 1
 store [x_in_UC] <-- 1  |   store [z_in_WC] <-- 1
 load [z_in_WC]         |   load [x_in_UC]

在这种情况下,store [z_in_WC] 可能直到 load [z_in_WC] 发生后才能到达物理地址空间(即使加载是在存储之后完成的)。两个负载都可能加载 0。

3a) 线程 0 |线程 1 存储 [x_in_UC]

与“1a”相同。不可能从 y 加载 1,从 x 加载 0。

3b)

   thread 0             |     thread 1
 store [x_in_WC] <-- 1  |   load [y_in_UC]
 store [y_in_UC] <-- 1  |   load [x_in_WC]

与“1b”相同。很可能从 y 加载 1,从 x 加载 0。

3c)

   thread 0             |     thread 1
 store [x_in_WC] <-- 1  |   load [y_in_UC]
 sfence                 |   load [x_in_WC]
 store [y_in_UC] <-- 1  |

sfence 强制线程 0 等待写入组合缓冲区耗尽,因此不可能从 y 加载 1,从 x 加载 0。

4a)

   thread 0             |     thread 1
 store [x_in_WB] <-- 1  |   load [y_in_WC]
 store [y_in_WC] <-- 1  |   load [x_in_WB]

大部分与“1a”和“3a”相同。唯一的区别是 x_in_WB 的存储进入缓存(而 x_in_WB 的加载来自缓存)。 注意:外部观察者(例如观察物理地址空间的设备)可能很长时间x_in_WB都看不到存储。

4b)

   thread 0             |     thread 1
 store [x_in_WC] <-- 1  |   load [y_in_WB]
 store [y_in_WB] <-- 1  |   load [x_in_WC]

大部分与“1b”和“3b”相同。 注意:外部观察者(例如观察物理地址空间的设备)可能很长时间y_in_WB都看不到存储。