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,AtomicBoolandOrderingare inlib/std, a hosted-only library. Each operation calls a runtime function inruntime/hosted/nova_rt_thread.c(nova_atomic_add_u64,nova_atomic_load_ord_u32, …). The ordered ones take the ordering as anintand 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/stdis not in a freestanding build, andlib/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_n593,__atomic_store_n393,__atomic_fetch_add141, …; counted 2026-10-29 inkernel/anddrivers/). 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 nolibatomicor__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 calls | B. Inline in the runtime header | C. Built-in operations in the emitter | |
|---|---|---|---|
| How | as today | the nova_atomic_* functions become static inline in rt.h; the ordering switch folds away where the ordering is a constant | the emitter writes __atomic_* with the ordering constant at each call site |
| Compiler change | none | none: the runtime and its header only | a new intrinsic kind in NIR and the emitter |
| An ordering not known at compile time | works | works: the switch stays | needs a fallback call |
-O0 | a call | a call to the inline function, with its switch | inline |
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
- B: inline atomics in the runtime header.
rt.hdefines thenova_atomic_*functions asstatic inline, with__atomic_*(gcc and clang) or_Interlocked*(MSVC), andnova_rt_thread.closes them. Nothing else in the generated C changes. - Atomics in every profile.
AtomicU64,AtomicU32,AtomicUSize,AtomicBoolandOrderingmove fromlib/stdtolib/core, so freestanding and kernel code has them, with the same API (type-system §38.3). TheirSend/Shareargument (@threadSafe) is unchanged.Arc,MutexandThreadstay inlib/std: they need the platform's threads and allocator. - 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 ownAtomicU64stops 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/atomicsis 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 anddocs/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
- Inline atomics through the runtime header (B)? (Recommended.)
- Move the atomic types and
Orderingtolib/core, so kernel code has them? (Recommended.)
6. Implementation and results (2026-10-29)
nova/rt.hdefines thenova_atomic_*functions asstatic inline:__atomic_*with constant orderings in a switch (gcc, clang),_Interlocked*intrinsics (MSVC).nova_rt_thread.cno longer has them, and a program that uses only atomics no longer links it. The interpreter is unchanged (CLibrary.cs).lib/core/atomics.novahasAtomicU64,AtomicU32,AtomicUSize,AtomicBool,LoadOrdering,StoreOrderingandOrdering, moved fromlib/std/sync.novaunchanged (doc comments added where lib/core requires them). Type-system §38.3 and §38.5 say so.The atomics'
Resulttypes 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. Theemit-csnapshots did not change.Freestanding (§3.3):
integrations/cosmoos/k3/atomics.novacalls every operation of every type, with every ordering kind, from statics.k3.pycompiles it with CosmoOS's module flags on both architectures, one file and split: the only undefined symbol is the kernel'spanic, so no operation calls a helper. On AArch64 the object holds the expected instructions:ldaxr/stlxrfor acquire-release and sequentially consistent read-modify-writes,ldxr/stxrfor relaxed ones,ldarandstlrfor loads and stores.Results.
benchmarks/atomicson the Ryzen box (as ADR 0021 §6: pinned to CPUs 1 and 2, median of 9, interleaved; load average 0.7–2.7):clang 18 gcc 12 Nova, acquire/release, through calls (ADR 0021 §6) 0.366 s 0.442 s Nova, acquire/release, inline 0.215 s 0.195 s C, acquire/release through calls 0.296 s 0.297 s C, acquire/release inline 0.160 s 0.157 s Nova, sequentially consistent 2.508 s 2.584 s C, sequentially consistent inline 2.816 s 2.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.