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.

§1. Release

1
2
3
4
5
6
7
8
9
10
int flag, sum;

void release(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

§2. Relaxed

1
2
3
4
5
6
7
8
void relaxed(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

§3. Relaxed + compiler barrier

1
2
3
4
5
6
7
8
9
void relaxed_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

§What the asm shows

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.

The complete source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdatomic.h>

int flag, sum;

void release(int n)
{
#pragma clang loop unroll(disable)
for (int i = 0; i < n; i += 2) {
sum += 1;
__atomic_store_n(&flag, i, __ATOMIC_RELEASE);
}
}

void relaxed(int n)
{
#pragma clang loop unroll(disable)
for (int i = 0; i < n; i += 2) {
sum += 1;
__atomic_store_n(&flag, i, __ATOMIC_RELAXED);
}
}

void relaxed_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);
}
}