QEMU atomic.h 具有以下定义:
#define smp_mb() ({ barrier(); __atomic_thread_fence(__ATOMIC_SEQ_CST); })
#define smp_mb_release() ({ barrier(); __atomic_thread_fence(__ATOMIC_RELEASE); })
#define smp_mb_acquire() ({ barrier(); __atomic_thread_fence(__ATOMIC_ACQUIRE); })
它有注释解释了为什么编译器障碍
barrier()
是必要的:
/* Manual memory barriers
*
*__atomic_thread_fence does not include a compiler barrier; instead,
* the barrier is part of __atomic_load/__atomic_store's "volatile-like"
* semantics. If smp_wmb() is a no-op, absence of the barrier means that
* the compiler is free to reorder stores on each side of the barrier.
* Add one here, and similarly in smp_rmb() and smp_read_barrier_depends().
*/
我以前没有使用过
__atomic_thread_fence
,但我在网上的搜索表明__atomic_thread_fence
可以防止编译器和CPU重新排序内存访问。例如,它的参考页面here和here并没有说这只是CPU障碍。 here 的答案明确指出它既是编译器障碍又是 CPU 障碍。
这是否意味着这些定义中的
barrier()
是多余的? (我只是好奇)
它与
__atomic_thread_fence(__ATOMIC_SEQ_CST);
是多余的,它不允许任何操作在任一方向上重新排序。 但没有什么坏处,所以最好还是保留它以保持一致性。
它与 RELEASE
或
ACQUIRE
栅栏不是多余。 从理论上讲,即使是
ACQ_REL
栅栏也允许对较早的商店和较晚的负载进行重新排序 (StoreLoad)。 因此,编译器可以在编译时执行此操作,并且不会发出指令来阻止它在运行时发生。
但是 Linux 内核对
smp_rmb()
和 smp_wmb()
的定义是根据 asm("..." ::: "memory")
GNU C 内联汇编来阻止所有编译时重新排序的。barrier()
定义为 asm("" ::: "memory")
。
实际上,GCC 可能将任何
__atomic_thread_fence
视为完整的编译器障碍;请参阅 gcc 是否将宽松的原子操作视为编译器栅栏? - GCC 目前甚至不会在 relaxed
操作之前和之后优化同一变量的增量。 但是 Clang 会优化。
int read_twice(int* x) {
int tmp = *x;
//barrier();
__atomic_thread_fence(__ATOMIC_RELEASE); // Doesn't block LoadLoad
tmp += *x;
return tmp;
}
最新的 GCC 加载两次。
Clang 正确地将其优化为没有
barrier()
的单个负载,但不能使用它。 (神箭)
# x86-64 clang 19, NO barrier()
read_twice(int*):
mov eax, dword ptr [rdi]
add eax, eax
ret
# x86-64 clang 19, WITH barrier()
read_twice_barrier(int*):
mov eax, dword ptr [rdi]
add eax, dword ptr [rdi]
ret
显然这是一个愚蠢的例子,其中屏障没有任何意义,但请记住,在内联小函数后可以进行优化。
没有
barrier()
就会中断的代码可能已经不安全了, 例如可能使用非原子(和非volatile
)访问共享变量而不同步。 在正确使用栅栏(和/或具有适当内存顺序的原子加载)的代码中,在没有 barrier()
的情况下允许的优化仍然是安全的。
另请参阅谁害怕一个糟糕的优化编译器?回复:对共享数据进行普通访问的危险:除了明显的陷阱之外,还可能存在微妙的影响,例如发明的加载,其中临时被优化掉并且编译器重新加载共享数据。
但无论如何,为了与 Linux 内核完全严格兼容
smp_*
内存屏障功能,阻止它们之间的所有编译时重新排序是正确的,而不是多余的。
相关:
__atomic
内置函数旨在实现相应的 C++ std::atomic
和 C 的行为stdatomic
功能,但有自己的文档。 (在这种情况下,这并没有说明任何问题,除了它不将它们记录为编译时重新排序的完全障碍这一事实之外。因此,这样的假设是不安全的。)