fix(rc): GREEN — atomic global g_rc_* stats fallback counters (swarm-safe)
bugfix-rc-global-stats-race, GREEN stage. RED is the separate
audit-trail commit 427b687.
g_rc_alloc_count / g_rc_free_count (runtime/rc.c:90-91) were plain
static uint64_t; the __ail_tls_ctx == NULL fallback at rc.c:161
(alloc) and rc.c:212 (dec-to-zero) did a non-atomic ++. A
multi-threaded host using the global fallback path (no
ailang_ctx_new) raced the read-modify-write and lost increments, so
the AILANG_RC_STATS atexit Σ under-counted non-deterministically.
Not a memory bug — Ctx:!Send keeps every box on one thread; only
the global statistics Σ was wrong (the M5 iter-2 symbol-fan
leak-proof finding, b724cd1; user-approved Option A bounce-back
resolution).
Fix (runtime/rc.c only, 37+/14-): the two globals are now
_Atomic uint64_t; atomic_fetch_add_explicit(.., memory_order_relaxed)
at the two null-ctx fallback ++ sites; atomic_load_explicit(..,
relaxed) snapshot in ailang_rc_stats_atexit (its sole reader).
Relaxed is correct and sufficient — pure statistics, no
happens-before obligation, atexit reader runs after all worker
threads joined. Per-ctx counters and the per-object refcount header
stay non-atomic BY DESIGN (single-thread-per-ctx; boxes never cross
threads) — out of scope, untouched. Frozen value layout / ABI
offsets / host-free rule untouched. The genuinely-still-
single-threaded drop-worklist doc note was correctly left unchanged
(false correction refused); the two stale atomicity doc blocks
corrected for honesty.
Boss-verified independently: RED -> GREEN deterministically 3/3
(jitter gone, allocs==frees==16_000_000); full cargo test -p ail
green (every binary 0 failed — per-ctx tsan harnesses + embed
e2e unaffected; rc.c links into every ail binary, strong regression
gate); scope = runtime/rc.c + journal + stats only; RED files
unchanged. Unblocks resuming the M5 leak-proof.
Includes the per-iter journal, stats, and the INDEX.md line.
This commit is contained in:
+37
-14
@@ -42,12 +42,20 @@
|
||||
* ABI" > "Frozen value layout". A boundary-crossing single-ctor
|
||||
* scalar record's box offsets MUST NOT move.
|
||||
*
|
||||
* Single-threaded: counter ops are non-atomic. AILang has no
|
||||
* concurrency primitives yet; when it acquires them, atomic-vs-non-
|
||||
* atomic becomes a separate decision per allocation kind (see
|
||||
* Decision 10's "Does not commit to atomic refcounts" clause).
|
||||
* Threading: the per-object refcount header ops and the per-ctx RC
|
||||
* counters are non-atomic by design — a box never crosses a thread
|
||||
* and each ailang_ctx_t is single-thread-per-ctx, so neither is
|
||||
* contended. The two GLOBAL null-ctx fallback stats counters
|
||||
* (g_rc_alloc_count / g_rc_free_count) ARE atomic (relaxed): a
|
||||
* multi-threaded host using the null-ctx fallback path concurrently
|
||||
* increments them, and the AILANG_RC_STATS atexit Σ must not lose
|
||||
* updates. AILang itself still has no concurrency primitives;
|
||||
* atomic-vs-non-atomic on the refcount header remains a separate
|
||||
* future decision per Decision 10's "Does not commit to atomic
|
||||
* refcounts" clause.
|
||||
*/
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -66,7 +74,7 @@ static inline ailang_rc_header_t *header_of(void *payload) {
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Iter 18g.0: opt-in alloc/free stats counter.
|
||||
*
|
||||
* Two non-atomic 64-bit counters incremented from `ailang_rc_alloc` and
|
||||
* Two atomic 64-bit counters incremented from `ailang_rc_alloc` and
|
||||
* the to-zero branch of `ailang_rc_dec`. Their difference is the live
|
||||
* cell count at any point in execution; at program exit a non-zero
|
||||
* difference is a leak.
|
||||
@@ -82,13 +90,22 @@ static inline ailang_rc_header_t *header_of(void *payload) {
|
||||
* and the bench numbers in JOURNAL 18f.2 were taken with the counters
|
||||
* compiled in but disabled, so the figures are still valid.
|
||||
*
|
||||
* Single-threaded; non-atomic. Same scope as the rest of `runtime/rc.c`.
|
||||
* The counters are intentionally not exposed via FFI symbols — the
|
||||
* Atomic (relaxed): a multi-threaded host can drive the null-ctx
|
||||
* fallback path concurrently, so these two counters use
|
||||
* `atomic_fetch_add_explicit(.., memory_order_relaxed)` on increment
|
||||
* and `atomic_load_explicit(.., memory_order_relaxed)` on read.
|
||||
* Relaxed is correct and sufficient: the counters are pure
|
||||
* statistics with no happens-before obligation, and the atexit
|
||||
* reader runs after all threads have joined (single-threaded at
|
||||
* exit). The per-ctx counters and the refcount header stay
|
||||
* non-atomic by design (single-thread-per-ctx; boxes never cross
|
||||
* threads). The counters are intentionally not exposed via FFI
|
||||
* symbols — the
|
||||
* env-var-gated atexit print is the supported readback path, and that
|
||||
* is sufficient for the e2e leak tests that consume the diagnostic.
|
||||
* --------------------------------------------------------------------------- */
|
||||
static uint64_t g_rc_alloc_count = 0;
|
||||
static uint64_t g_rc_free_count = 0;
|
||||
static _Atomic uint64_t g_rc_alloc_count = 0;
|
||||
static _Atomic uint64_t g_rc_free_count = 0;
|
||||
|
||||
/* M2: per-thread embedding context. A scalar kernel allocates nothing,
|
||||
* so this carries only the de-globalised RC accounting. Set by the
|
||||
@@ -122,11 +139,15 @@ void ailang_ctx_free(ailang_ctx_t *ctx) {
|
||||
}
|
||||
|
||||
static void ailang_rc_stats_atexit(void) {
|
||||
uint64_t allocs =
|
||||
atomic_load_explicit(&g_rc_alloc_count, memory_order_relaxed);
|
||||
uint64_t frees =
|
||||
atomic_load_explicit(&g_rc_free_count, memory_order_relaxed);
|
||||
fprintf(stderr,
|
||||
"ailang_rc_stats: allocs=%llu frees=%llu live=%lld\n",
|
||||
(unsigned long long)g_rc_alloc_count,
|
||||
(unsigned long long)g_rc_free_count,
|
||||
(long long)(g_rc_alloc_count - g_rc_free_count));
|
||||
(unsigned long long)allocs,
|
||||
(unsigned long long)frees,
|
||||
(long long)(allocs - frees));
|
||||
}
|
||||
|
||||
__attribute__((constructor))
|
||||
@@ -158,7 +179,8 @@ void *ailang_rc_alloc(size_t size) {
|
||||
void *payload = (uint8_t *)block + HEADER_SIZE;
|
||||
memset(payload, 0, size);
|
||||
ailang_ctx_t *_ctx = __ail_tls_ctx;
|
||||
if (_ctx != NULL) _ctx->alloc_count++; else g_rc_alloc_count++;
|
||||
if (_ctx != NULL) _ctx->alloc_count++;
|
||||
else atomic_fetch_add_explicit(&g_rc_alloc_count, 1, memory_order_relaxed);
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -209,7 +231,8 @@ void ailang_rc_dec(void *payload) {
|
||||
if (*hdr == 0) {
|
||||
free(hdr);
|
||||
ailang_ctx_t *_ctx = __ail_tls_ctx;
|
||||
if (_ctx != NULL) _ctx->free_count++; else g_rc_free_count++;
|
||||
if (_ctx != NULL) _ctx->free_count++;
|
||||
else atomic_fetch_add_explicit(&g_rc_free_count, 1, memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user