Skip to content

ADR 0026 — M14 K5: inline atomics, and atomics in every profile ​

  • Status: Accepted (project owner, 2026-10-29), both questions as recommended (§5). The wording is the agent's.
  • Scope: OD-27 (M14 K5's first item). How the atomic types' operations reach the machine, and which profiles have them.

1. Context ​

  • Today (ADR 0016, ADR 0021): AtomicU64, AtomicU32, AtomicUSize, AtomicBool and Ordering are in lib/std, a hosted-only library. Each operation calls a runtime function in runtime/hosted/nova_rt_thread.c (nova_atomic_add_u64, nova_atomic_load_ord_u32, …). The ordered ones take the ordering as an int and switch on it.
  • The cost of the call (ADR 0021 §1, benchmarks/atomics, Ryzen box, x86-64): the ring with acquire/release takes 0.289 s through the runtime calls and 0.158 s written in C with inline __atomic_*, a further 1.8×. Sequentially consistent inline C was no faster than the calls (2.761 s against 2.170 s), because the fence dominates.
  • The kernel has no Nova atomics. lib/std is not in a freestanding build, and lib/kernel (ADR 0025) has none. Roadmap 2.0's SA track converts CosmoOS's C, which uses the GCC builtins directly: 1,233 __atomic_* calls in 98 files (__atomic_load_n 593, __atomic_store_n 393, __atomic_fetch_add 141, …; counted 2026-10-29 in kernel/ and drivers/). A converted subsystem needs atomics of its own.
  • No library calls. CosmoOS builds AArch64 code with -mno-outline-atomics, and so does Nova's freestanding profile (cosmoos/profiles.md §4.3). With it, gcc and clang expand every 8-, 16-, 32- and 64-bit __atomic_* on both architectures inline, with no libatomic or __aarch64_* helper (inferred from CosmoOS's own use; checked per architecture by the implementation, below). MSVC has the _Interlocked* intrinsics, already used by the runtime.

2. Options ​

A. Keep the callsB. Inline in the runtime headerC. Built-in operations in the emitter
Howas todaythe nova_atomic_* functions become static inline in rt.h; the ordering switch folds away where the ordering is a constantthe emitter writes __atomic_* with the ordering constant at each call site
Compiler changenonenone: the runtime and its header onlya new intrinsic kind in NIR and the emitter
An ordering not known at compile timeworksworks: the switch staysneeds a fallback call
-O0a calla call to the inline function, with its switchinline
Gain at -O2—as C (expected; to be measured)as C

B gives what C gives at -O2, where the ordering at a call site is a constant (it is in every …With call written with an enum constant, after inlining), with no change to the compiler.

3. Decision ​

  1. B: inline atomics in the runtime header. rt.h defines the nova_atomic_* functions as static inline, with __atomic_* (gcc and clang) or _Interlocked* (MSVC), and nova_rt_thread.c loses them. Nothing else in the generated C changes.
  2. Atomics in every profile. AtomicU64, AtomicU32, AtomicUSize, AtomicBool and Ordering move from lib/std to lib/core, so freestanding and kernel code has them, with the same API (type-system §38.3). Their Send/Share argument (@threadSafe) is unchanged. Arc, Mutex and Thread stay in lib/std: they need the platform's threads and allocator.
  3. The freestanding profile's rule is checked. A test compiles a program using every atomic operation with the freestanding profile's flags for both CosmoOS architectures and requires no undefined symbol (as C4's checks do, c4.py).

4. Consequences ​

  • lib/core's names are reserved in every package: a freestanding package that declares its own AtomicU64 stops compiling (none of CosmoOS's four ports does).
  • The kernel's Nova code uses the same atomics as userland Nova code, compiled as CosmoOS's C compiles its own. lib/kernel's future reference counts and flags use them.
  • benchmarks/atomics is rerun on the Ryzen box after the change: the Nova acquire/release row against the C inline row, and the sequentially consistent rows, under clang and gcc. The result goes into §6 and docs/benchmarks.md. If the gap to C does not close, C (built-in operations) is reconsidered.
  • Not decided here: atomics on pointers and on 8- and 16-bit integers (add as converted code needs them), and fences as a library function (atomicFence(order)), which CosmoOS's C uses 22 times.

5. Questions for the owner ​

  1. Inline atomics through the runtime header (B)? (Recommended.)
  2. Move the atomic types and Ordering to lib/core, so kernel code has them? (Recommended.)

6. Implementation and results (2026-10-29) ​

  • nova/rt.h defines the nova_atomic_* functions as static inline: __atomic_* with constant orderings in a switch (gcc, clang), _Interlocked* intrinsics (MSVC). nova_rt_thread.c no longer has them, and a program that uses only atomics no longer links it. The interpreter is unchanged (CLibrary.cs).

  • lib/core/atomics.nova has AtomicU64, AtomicU32, AtomicUSize, AtomicBool, LoadOrdering, StoreOrdering and Ordering, moved from lib/std/sync.nova unchanged (doc comments added where lib/core requires them). Type-system §38.3 and §38.5 say so.

  • The atomics' Result types would have been emitted in every program, because a generic instance made by lib/core's own code counted as the program's. Now an instance made only by a library's code (lib/core, as lib/std and lib/kernel already were) is emitted only where the program uses it. The emit-c snapshots did not change.

  • Freestanding (§3.3): integrations/cosmoos/k3/atomics.nova calls every operation of every type, with every ordering kind, from statics. k3.py compiles it with CosmoOS's module flags on both architectures, one file and split: the only undefined symbol is the kernel's panic, so no operation calls a helper. On AArch64 the object holds the expected instructions: ldaxr/stlxr for acquire-release and sequentially consistent read-modify-writes, ldxr/stxr for relaxed ones, ldar and stlr for loads and stores.

  • Results. benchmarks/atomics on the Ryzen box (as ADR 0021 §6: pinned to CPUs 1 and 2, median of 9, interleaved; load average 0.7–2.7):

    clang 18gcc 12
    Nova, acquire/release, through calls (ADR 0021 §6)0.366 s0.442 s
    Nova, acquire/release, inline0.215 s0.195 s
    C, acquire/release through calls0.296 s0.297 s
    C, acquire/release inline0.160 s0.157 s
    Nova, sequentially consistent2.508 s2.584 s
    C, sequentially consistent inline2.816 s2.116 s

    The ordered ring is 1.7× (clang) and 2.3× (gcc) faster than through calls, and within 1.34× and 1.24× of inline C. Nova's ring is now faster than C's through calls. Sequentially consistent rings are dominated by their fences, as before, and vary by tens of percent between runs. Option C (built-in operations) is not needed for this gap: what remains is the ring's own code (inferred: the indexing and its checks around the atomics), not the atomics.

Pre-1.0: the language and its tools are still changing.