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.
$ git clone https://github.com/adit4443ya/debug_labs.git && cd debug_labs
$ (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.
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.
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. ============================================================
#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; }A lab is done when you can say what it showed on your machine — not when you have read it.
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.