我正在看gcc 的 rmw 原子的编译器输出 https://goo.gl/ZWLeCJ并注意到一些奇怪的事情 - 在 Aarch64 上,诸如 fetch_add 之类的 rmw 操作可以通过宽松的负载进行部分重新排序。
在 Aarch64 上,可能会生成以下代码value.fetch_add(1, seq_cst)
.L1:
ldaxr x1, [x0]
add x1, x1, 1
stlxr w2, x1, [x0]
cbnz L1
但是,在 ldaxr 之前发生的加载和存储可能会被重新排序,超过 stlxr 之后发生的加载和加载/存储(请参阅here http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/CJAIAJFI.html)。 GCC 不会添加围栏来防止这种情况 - 下面是一小段代码演示了这一点:
void partial_reorder(std::atomic<uint64_t> loader, std::atomic<uint64_t> adder) {
loader.load(std::memory_order_relaxed); // can be reordered past the ldaxr
adder.fetch_add(1, std::memory_order_seq_cst);
loader.load(std::memory_order_relaxed); // can be reordered past the stlxr
}
生成
partial_reorder(std::atomic<int>, std::atomic<int>):
ldr w2, [x0] @ reordered down
.L2:
ldaxr w2, [x1]
add w2, w2, 1
stlxr w3, w2, [x1]
cbnz w3, .L2
ldr w0, [x0] @ reordered up
ret
实际上,负载可以通过 RMW 操作进行部分重新排序 - 它们发生在 RMW 操作的中间。
那么,有什么大不了的呢?我在问什么?
原子操作本身是可分的,这似乎很奇怪。我在标准中找不到任何阻止这种情况的内容,但我相信存在隐含操作不可分割的规则组合。
这似乎不尊重获取顺序。如果我在此操作之后直接执行加载,我可以看到 fetch_add 和后面的操作之间的存储-加载或存储-存储重新排序,这意味着后面的内存访问在获取操作之后至少部分地重新排序。同样,我在标准中找不到任何明确说明不允许这样做的内容,并且获取是加载排序,但我的理解是获取操作适用于整个操作,而不仅仅是其中的一部分。类似的场景也适用于通过 ldaxr 重新排序某些内容的版本。
这可能会进一步扩展排序定义,但 seq_cst 操作之前和之后的两个操作可以相互重新排序似乎是无效的。如果边界操作各自重新排序到操作的中间,然后相互超越,则可能会发生这种情况。