FForge
← Labsconcurrency-03Concurrency●●○○

What each memory_order compiles to on x86-64 and AArch64

concurrency/03_memory_orders_asm.cneeds: clang -S for both targets (aarch64 via tools/a64.sh sysroot)
what this lab teaches

The C11/C++11 memory orders are abstract; the cost is concrete and depends on the architecture's memory model. Thirteen tiny functions, one operation each, compiled for x86-64 (TSO: strong), AArch64 v8.0 (weak, exclusives) and AArch64 v8.1+ (LSE atomics). No running needed: this lab is entirely about reading the .s files build.sh emits.

evidence to leave with
The 13-row table; only x86 seq_cst stores pay; Arm pays for anything above relaxed
first, get the code
run this
$ git clone https://github.com/adit4443ya/debug_labs.git && cd debug_labs
then build it
run this
$ (cd concurrency && ./build.sh)   -> bin/03_x86.s, bin/03_aarch64.s, bin/03_aarch64_lse.s
$ 
$ THE TABLE (one line per function, from the three .s files):
$ 
$   function        x86-64 (TSO)                          AArch64 v8.0 (exclusives)                     AArch64 v8.1 LSE
$   ------------    ----------------------------------    -------------------------------------------   ----------------------------
$   load_relaxed    movl (%rdi),%eax                      ldr  w0,[x0]                                  (same)
$   load_acquire    movl (%rdi),%eax                      ldar w0,[x0]                                  (same)
$   load_seqcst     movl (%rdi),%eax                      ldar w0,[x0]                                  (same)
$   store_relaxed   movl %esi,(%rdi)                      str  w1,[x0]                                  (same)
$   store_release   movl %esi,(%rdi)                      stlr w1,[x0]                                  (same)
$   store_seqcst    xchgl %esi,(%rdi)                     stlr w1,[x0]                                  (same)
$   add_relaxed     lock xaddl %eax,(%rdi)                loop: ldxr; add; stxr; cbnz                   ldadd  w8,w0,[x0]
$   add_acqrel      lock xaddl %eax,(%rdi)                loop: ldaxr; add; stlxr; cbnz                 ldaddal w8,w0,[x0]
$   cas_seqcst      lock cmpxchgl %edx,(%rdi); sete       loop: ldaxr; cmp; b.ne; stlxr; cbnz; clrex    casal  w8,w2,[x0]; cmp; cset
$   fence_release   (nothing)                             dmb ish                                       (same)
$   fence_seqcst    lock orl $0,-64(%rsp)                 dmb ish                                       (same)
$   plain_load      movl (%rdi),%eax                      ldr  w0,[x0]                                  (same)
$   plain_store     movl %esi,(%rdi)                      str  w1,[x0]                                  (same)
$ 
$ HOW TO READ IT, x86:
$   - relaxed, acquire, release and even seq_cst LOADS are plain movs: TSO
$     already forbids the reorderings those orders forbid. The atomic
$     versions differ from plain_load only in what the COMPILER may do
$     (no tearing, no caching in registers, no reordering with other
$     atomics); the hardware instruction is identical.
$   - store_seqcst is xchg: an implicitly locked instruction, i.e. a full
$     fence that drains the store buffer. It is the one order that costs on
$     x86, ~20-40 cycles, and it is the default. (Lab 05 shows why it exists:
$     without it, StoreLoad reordering is visible.)
$   - RMWs are lock-prefixed regardless of order: on x86 every atomic RMW
$     is already sequentially consistent. relaxed buys nothing on x86 for
$     fetch_add; it buys a lot on AArch64.
$   - fence_seqcst is `lock orl $0,(%rsp)`: a locked no-op used as a cheaper
$     mfence. fence_release emits nothing: the compiler barrier is enough.
$ 
$ HOW TO READ IT, AArch64:
$   - The memory model is weak: plain ldr/str may reorder with anything.
$     acquire and release are separate INSTRUCTIONS: ldar (load-acquire),
$     stlr (store-release). They cost real cycles and they are exactly what
$     you asked for; relaxed gets the cheap ldr/str.
$   - seq_cst maps to the SAME ldar/stlr as acquire/release: the pair is
$     RCsc (sequentially consistent with each other) by design, so seq_cst
$     is not more expensive than acq/rel on AArch64. The cost cliff is
$     between relaxed and everything else, the opposite of x86.
$   - v8.0 RMWs are load-exclusive / store-exclusive LOOPS (ldxr/stxr): the
$     store fails and retries if another core touched the line in between.
$     The a/l suffixes add acquire/release: ldaxr/stlxr. Under contention
$     these loops livelock-ish and burn cycles.
$   - v8.1 LSE replaces the loop with ONE instruction: ldadd / cas / swp,
$     with al suffixes for acq_rel (ldaddal, casal). That is why every Arm
$     server core since Neoverse N1 has LSE and why -moutline-atomics exists
$     (aarch64 lab 03). The x86 lock-prefix and LSE are the same idea:
$     the RMW is done by the cache system, not by a retry loop.
$   - fences are dmb ish (data memory barrier, inner-shareable domain):
$     both release and seq_cst fences are the same full barrier here; the
$     compiler does not have a cheaper one-directional fence to reach for.
the rules

O1. On x86, relaxed vs acquire/release changes NOTHING in the instructions except for stores at seq_cst (xchg). Use acq/rel to document intent and to keep the code correct on Arm; do not expect a speedup on x86 from "relaxing" loads. O2. On AArch64, relaxed is genuinely cheaper; acq/rel/seq_cst cost the same. Choose by correctness (lab 05 shows when seq_cst is required). O3. Prefer RMW instructions over CAS loops (fetch_add over compare_exchange in a loop): on both architectures they are single instructions that cannot fail. O4. When in doubt, compile with -S for both targets and READ. It takes ten seconds and settles most arguments.

take it further

1. Add `exchange` and `fetch_or`: x86 xchg / lock cmpxchg loop (or is not a lock-able single instruction with a result), AArch64 swp / ldset. 2. Compile for -march=armv8-a with -moutline-atomics: every RMW becomes `bl __aarch64_ldadd4_relax`: a runtime-dispatched helper (aarch64 lab 03). 3. Compile for x86 with -mcx16 and use a 16-byte atomic: lock cmpxchg16b. ============================================================

read the source · concurrency/03_memory_orders_asm.c
#include <stdatomic.h> #include <stdint.h> int load_relaxed(atomic_int *p) { return atomic_load_explicit(p, memory_order_relaxed); } int load_acquire(atomic_int *p) { return atomic_load_explicit(p, memory_order_acquire); } int load_seqcst (atomic_int *p) { return atomic_load_explicit(p, memory_order_seq_cst); } void store_relaxed(atomic_int *p, int v) { atomic_store_explicit(p, v, memory_order_relaxed); } void store_release(atomic_int *p, int v) { atomic_store_explicit(p, v, memory_order_release); } void store_seqcst (atomic_int *p, int v) { atomic_store_explicit(p, v, memory_order_seq_cst); } int add_relaxed(atomic_int *p) { return atomic_fetch_add_explicit(p, 1, memory_order_relaxed); } int add_acqrel (atomic_int *p) { return atomic_fetch_add_explicit(p, 1, memory_order_acq_rel); } int cas_seqcst(atomic_int *p, int expect, int desired) { return atomic_compare_exchange_strong_explicit(p, &expect, desired, memory_order_seq_cst, memory_order_seq_cst); } void fence_release(void) { atomic_thread_fence(memory_order_release); } void fence_seqcst (void) { atomic_thread_fence(memory_order_seq_cst); } /* Non-atomic for contrast: a plain int load/store. */ int plain_load(int *p) { return *p; } void plain_store(int *p, int v) { *p = v; }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.