我想知道,
std::memory_ordering
类型的参数的值只是提示编译器如何重新排序代码,还是该值也影响操作原子对象的指令的选择?
如https://en.cppreference.com/w/cpp/atomic/memory_order中所述,例如:
:在此加载之前,当前线程中的任何读取或写入都不能重新排序。memory_order_acquire
这对编译器提出了如何对代码重新排序的要求。假设目标平台有两个原子指令:
W0 addr, eax
、W1 addr, eax
。 IIUC,memory_order
的值也会影响原子对象使用哪条指令的选择,对吧?
另一个问题是,如果
memory_order
类型的参数的值只能在运行时确定,那么编译器如何知道如何根据该值对代码进行重新排序?
是的,两者都有。
C++内存模型要求原子操作遵循一定的语义,这取决于指定的内存排序参数。因此,编译器必须发出在执行时根据这些语义表现的代码。
例如,采用如下代码:
std::atomic<int> x;
int y, tmp;
if (x.load(std::memory_order_acquire) == 5) {
tmp = y;
}
在典型的机器上,编译器需要:
不在编译时重新排序
x
和 y
的加载。换句话说,它应该发出一条加载指令 x
和一条加载指令 y
,以便按照程序顺序,第一个指令在第二个指令之前执行。
确保
x
和y
的负载按顺序变为visible。如果机器能够执行无序执行、推测性加载或任何其他可能导致两个加载不按程序顺序可见的功能,则编译器必须发出代码以防止这种情况在这种情况下发生。
该代码的外观取决于相关机器。可能性包括:
不需要什么特别的,因为机器不会进行这种特殊的重新排序。因此
x
和 y
将仅由普通加载指令加载,没有任何额外的内容。例如,x86 上就是这种情况,其中“所有负载均已获取”。
使用特殊形式的加载指令来禁止重新排序。例如,在 AArch64 上,
x
的加载将使用 ldapr
或 ldar
指令而不是普通的 ldr
来完成。
在两个负载之间插入特殊的内存屏障指令,就像ARM的
dmb
。
在绝大多数代码中,内存排序参数被指定为编译时常量,因为程序员静态地知道需要什么排序,因此编译器可以发出适合该特定排序的指令。
在排序参数不是常量的特殊情况下,编译器必须发出无论指定什么值都将正常运行的代码。通常所做的是,编译器只是将排序参数视为
memory_order_seq_cst
,因为它比所有其他参数都更强:seq_cst
操作满足较弱排序所需的所有语义(以及更多)。这节省了在运行时实际测试排序参数值并相应分支的成本,这可能超过使用较弱排序执行操作所节省的潜在成本。
但是,如果编译器确实选择测试和分支,那么为了优化周围代码,它通常必须假设“最坏情况”。例如,在 AArch64 上,对于
x.load(order)
,它可能会发出如下代码块:
int t;
if (order == std::memory_order_relaxed)
LDR t, [x]
else if (order == std::memory_order_acquire)
LDAPR t, [x]
else if (order == std::memory_order_seq_cst)
LDAR t, [x]
else
abort();
if (t == 5)
LDR tmp, [y]
但是,需要确保
y
的负载保持在该代码块的末尾(按程序顺序)。如果 order
等于 std::memory_order_relaxed
,那么可以在 y
的加载之前执行 x
的加载,但如果它是 std::memory_order_acquire
或更强,则不行。
另一方面,它可能会发射
int t, t2;
if (order == std::memory_order_relaxed) {
LDR t2, [y]
LDR t, [x]
} else if (order == std::memory_order_acquire) {
LDAPR t, [x]
LDR t2, [y]
} else if (order == std::memory_order_seq_cst) {
LDAR t, [x]
LDR t2, [y]
else
abort();
if (t == 5)
tmp = t2;
但我们现在远远超出了现实世界编译器实际执行的转换范围。