如何解决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
都看不到存储。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。