GCC and Clang provide __atomic_* builtins for atomic access to shared data in C and C++ programs. Each operation’s memory-order argument sets the ordering constraints on surrounding accesses. The call-like syntax may suggest an opaque function, but the compiler knows each builtin’s semantics and may move code when that order allows it, so these builtins are not general compiler barriers.
To see the difference, compare three versions of the same loop.
voidrelease(int n) { #pragma clang loop unroll(disable) for (int i = 0; i < n; i += 2) { sum += 1; __atomic_store_n(&flag, i, __ATOMIC_RELEASE); } }
The following asm excerpts use Intel syntax; unrelated setup and cleanup are omitted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
_release: ... mov ecx, dword ptr [rax] ; load sum inc ecx ; sum += 1 xor edx, edx ; i = 0 ... LBB0_2: mov dword ptr [rax], ecx ; write sum before flag mov dword ptr [rsi], edx ; release store add edx, 2 ; i += 2 inc ecx ; next sum value cmp edx, edi ; compare i with n jl LBB0_2 ; loop back ... LBB0_4: ret
voidrelaxed(int n) { #pragma clang loop unroll(disable) for (int i = 0; i < n; i += 2) { sum += 1; __atomic_store_n(&flag, i, __ATOMIC_RELAXED); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
_relaxed: ... mov ecx, dword ptr [rax] ; load sum before the loop xor edx, edx ; i = 0 ... LBB1_2: mov dword ptr [rsi], edx ; relaxed store add edx, 2 ; i += 2 cmp edx, edi ; compare i with n jl LBB1_2 ; loop back dec edi ; calculate iteration count shr edi, 1 add ecx, edi inc ecx mov dword ptr [rax], ecx ; write sum after the loop LBB1_4: ret
voidrelaxed_barrier(int n) { #pragma clang loop unroll(disable) for (int i = 0; i < n; i += 2) { sum += 1; __asm__ __volatile__("" ::: "memory"); __atomic_store_n(&flag, i, __ATOMIC_RELAXED); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
_relaxed_barrier: ... xor eax, eax ; i = 0 ... LBB2_2: inc dword ptr [rcx] ; sum += 1, in the loop ## InlineAsm Start ; memory clobber ## InlineAsm End mov dword ptr [rdx], eax ; relaxed store add eax, 2 ; i += 2 cmp eax, edi ; compare i with n jl LBB2_2 ; loop back ... LBB2_4: ret
With release or a compiler barrier, each update to sum stays before the corresponding store to flag. With relaxed ordering, clang moves the update out of the loop and writes sum once afterward.
On x64, a release store is a plain mov. The difference here is compiler ordering, not a hardware fence.
The pragma prevents unrolling, so each asm block shows one loop body. Without it, clang still moves the relaxed update out of the loop.
__atomic_store_n is the example here. Other __atomic operations also use their requested memory order to constrain compiler motion.
Environment: Apple clang 21 on an arm64 Mac, targeting x86_64 with -arch x86_64 -O2. The assembly uses Apple’s x64 ABI.