lis*_*reg 5 c++ arm memory-model memory-barriers stdatomic
我不是 ARM 专家,但至少在某些 ARM 架构上不会对这些存储和加载进行重新排序吗?
atomic<int> atomic_var;
int nonAtomic_var;
int nonAtomic_var2;
void foo()
{
atomic_var.store(111, memory_order_relaxed);
atomic_var.store(222, memory_order_relaxed);
}
void bar()
{
nonAtomic_var = atomic_var.load(memory_order_relaxed);
nonAtomic_var2 = atomic_var.load(memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)
我没有成功地让编译器在它们之间放置内存屏障。
我试过如下(在 x64 上):
$ arm-linux-gnueabi-g++ -mcpu=cortex-a9 -std=c++11 -S -O1 test.cpp
Run Code Online (Sandbox Code Playgroud)
我有:
_Z3foov:
.fnstart
.LFB331:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
mov r2, #111
str r2, [r3]
mov r2, #222
str r2, [r3]
bx lr
;...
_Z3barv:
.fnstart
.LFB332:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
ldr r2, [r3]
str r2, [r3, #4]
ldr r2, [r3]
str r2, [r3, #8]
bx lr
Run Code Online (Sandbox Code Playgroud)
是否从未在 ARM 上重新排序到同一位置的加载和存储?我在 ARM 文档中找不到这样的限制。
我问的是 c++11 标准,该标准规定:
对任何特定原子变量的所有修改都以特定于该原子变量的总顺序发生。
由于缓存一致性 (MESI),单个变量的总顺序存在:除非内核拥有对该缓存行的独占访问权限,否则存储无法从存储缓冲区提交到 L1d 缓存并对其他线程全局可见。(MESI 独占或修改状态。)
C++ 保证不需要任何障碍来在任何普通 CPU 架构上实现,因为所有普通 ISA 都有一致的缓存,通常使用 MESI 的变体。这就是为什么volatile
碰巧作为mo_relaxed
atomic
主流 C++ 实现的遗留/UB 版本工作的原因(但通常不这样做)。另请参阅何时将 volatile 与多线程一起使用?更多细节。
(有些系统存在两种不同类型的共享内存的 CPU,例如微控制器 + DSP,但 C++std::thread
不会在不共享内存的一致视图的内核之间启动线程。 因此编译器只需要为ARM 内核在同一内部共享一致性域中。)
对于任何给定的原子对象,所有线程的总修改顺序将始终存在(正如您引用的 ISO C++ 标准所保证的那样),但是除非您在线程之间建立同步,否则您无法提前知道它将是什么。
例如,该程序的不同运行可以先加载两个加载,或者一个加载然后两个存储然后另一个加载。
此总顺序(对于单个变量)将与每个线程的程序顺序兼容,但它是程序顺序的任意交错。
memory_order_relaxed
仅对该变量进行原子操作,而不是对 wrt 进行排序。还要别的吗。 编译时唯一固定的顺序是wrt。此线程对同一原子变量的其他访问。
不同的线程会同意这个变量的修改顺序,但可能会不同意所有对象的全局修改顺序。(ARMv8 使 ARM 内存模型具有多副本原子性,因此这是不可能的(并且可能没有真正的早期 ARM 违反了这一点),但是 POWER 在现实生活中确实允许两个独立的读取器线程对另外 2 个独立写入器的存储顺序产生分歧线程。这称为 IRIW 重新排序。 对不同线程中不同位置的两次原子写入是否总是被其他线程以相同的顺序看到?)
事实上,IRIW重新排序是当多个变量是涉及有(其中包括)为什么它甚至需要说的是一个总的修改订单的可能性确实总是单独存在于每个变量。
为了存在全线程全序,您需要使用所有原子访问seq_cst
,这将涉及障碍。但这当然仍然不能在编译时完全确定该顺序是什么;不同运行的不同时间将导致获取负载看到某个商店与否。
是否从未在 ARM 上重新排序到同一位置的加载和存储?
从单个线程中没有。如果您对一个内存位置进行多次存储,则程序顺序中的最后一个将始终显示为其他线程的最后一个。即一旦尘埃落定,内存位置将具有上次存储存储的值。其他任何事情都会打破线程重新加载自己的存储的程序顺序的错觉。
C++ 标准中的一些排序保证甚至被称为“写-写一致性”和其他类型的一致性。ISO C++ 没有明确要求一致的缓存(在需要显式刷新的 ISA 上的实现是可能的),但效率不高。
http://eel.is/c++draft/intro.races#19
[注意:前面的四个一致性要求有效地禁止编译器将原子操作重新排序为单个对象,即使这两个操作都是宽松加载。这有效地使大多数硬件提供的缓存一致性保证可用于 C++ 原子操作。— 尾注 ]
以上大部分是关于修改顺序,而不是 LoadLoad 重新排序。
那是另外一回事。C++ 保证读-读一致性,即同一线程对同一原子对象的 2 次读取以程序顺序相对于彼此发生。
http://eel.is/c++draft/intro.races#16
如果原子对象 M 的值计算 A 发生在 M 的值计算 B 之前,并且 A 从 M 上的副作用 X 获取其值,则 B 计算的值应为 X 存储的值或存储的值由于 Y 对 M 的副作用,其中 Y 按照 M 的修改顺序跟在 X 之后。[注意:此要求称为读-读一致性。— 尾注 ]
“值计算”是对变量的读取又名加载。突出显示的短语是保证同一线程中的后续读取无法观察到来自其他线程的较早写入(早于他们已经看到的写入)的部分。
这是我之前链接的引用所谈论的 4 个条件之一。
编译器将其编译为两个普通 ARM 负载的事实足以证明 ARM ISA 也能保证这一点。 (因为我们确信 ISO C++ 需要它。)
我不熟悉 ARM 手册,但大概在某处。
另请参阅ARM 和 POWER 松弛内存模型的教程介绍- 这篇论文详细介绍了各种测试用例允许/不允许重新排序的内容。