bench: 21'f — explicit-mode pair, full alloc+dec vs malloc+free

Closes the apples-to-apples gap from 21'e. Adds:
- examples/bench_list_sum_explicit.ailx — same algorithm and sizes
  as bench_list_sum, fully (borrow)/(own)/(drop-iterative)
  annotated so codegen emits proper inc/dec instrumentation.
- bench/reference/list_sum_explicit_free.c — same algorithm
  with explicit free() walking the chain after sum.

The full alloc+dec vs malloc+free comparison reveals two non-
trivial conclusions:

1. AILang's full RC pipeline is only 26% slower than glibc
   malloc+free on this workload (rc/c = 1.26x). The implicit-
   mode comparison's 1.42x was misleading — it counted neither
   pipeline's free path. The fair ratio is 1.26x, materially
   better than the previous read.

2. RC's dec is cheaper per cell than glibc free(). AILang
   dec-tax: ~3 ns/cell. C free-tax: ~5.5 ns/cell. Plausible
   cause: ailang_rc_dec operates on a known-shape cell with a
   fixed-offset refcount and a static per-type drop fn — no
   free-list bucketing, no header introspection, no global lock.

bump's advantage expresses fully: bench_list_sum_explicit.bump/c
= 0.42x means AILang at bump is 2.4x faster than C malloc+free.
Sets a useful upper bound on a slab/pool RC allocator's potential.

The 21'-family arc — bench-regression infrastructure — is now
substantively complete: 21'a (bench/check.py), 21'b (corpus
widening), 21'c (compile_check.py), 21'd (pure-compute fixtures
+ harness hardening), 21'e (cross-language hand-C), 21'f (explicit
apples-to-apples). 63 runtime metrics + 18 compile metrics + 25
cross-lang metrics under regression coverage. Any future iter
that regresses any axis beyond tolerance gets caught at the next
family close.

Remaining queue is back to substantive language work — Family 21
(typeclasses / polymorphic ADTs at runtime / pattern-binding
generalisation) is now an orchestrator-level fork that needs
direct user input.
This commit is contained in:
2026-05-09 01:21:15 +02:00
parent c897d2eef0
commit 75f7fda788
8 changed files with 369 additions and 25 deletions
+10
View File
@@ -54,6 +54,16 @@
"gc_rss_kb": { "baseline": 13624, "tolerance_pct": 15 },
"bump_rss_kb": { "baseline": 13860, "tolerance_pct": 15 },
"rc_rss_kb": { "baseline": 13880, "tolerance_pct": 15 }
},
"bench_list_sum_explicit": {
"gc_s": { "baseline": 0.139, "tolerance_pct": 10 },
"bump_s": { "baseline": 0.048, "tolerance_pct": 10 },
"rc_s": { "baseline": 0.152, "tolerance_pct": 10 },
"gc_over_bump": { "baseline": 2.89, "tolerance_pct": 8 },
"rc_over_bump": { "baseline": 3.14, "tolerance_pct": 8 },
"gc_rss_kb": { "baseline": 103772, "tolerance_pct": 5 },
"bump_rss_kb": { "baseline": 97636, "tolerance_pct": 5 },
"rc_rss_kb": { "baseline": 142424, "tolerance_pct": 8 }
}
},
+42 -20
View File
@@ -6,89 +6,111 @@
"fixtures": {
"bench_list_sum": {
"ail_rc_s": {
"baseline": 0.141597,
"baseline": 0.138906,
"tolerance_pct": 15
},
"ail_bump_s": {
"baseline": 0.048038,
"baseline": 0.050245,
"tolerance_pct": 15
},
"c_s": {
"baseline": 0.095312,
"baseline": 0.09749,
"tolerance_pct": 15
},
"rc_over_c": {
"baseline": 1.485614,
"baseline": 1.42483,
"tolerance_pct": 12
},
"bump_over_c": {
"baseline": 0.504003,
"baseline": 0.515393,
"tolerance_pct": 12
}
},
"bench_tree_walk": {
"ail_rc_s": {
"baseline": 0.097028,
"baseline": 0.100173,
"tolerance_pct": 15
},
"ail_bump_s": {
"baseline": 0.038905,
"baseline": 0.038673,
"tolerance_pct": 15
},
"c_s": {
"baseline": 0.037159,
"baseline": 0.036567,
"tolerance_pct": 15
},
"rc_over_c": {
"baseline": 2.611185,
"baseline": 2.739449,
"tolerance_pct": 12
},
"bump_over_c": {
"baseline": 1.046998,
"baseline": 1.057606,
"tolerance_pct": 12
}
},
"bench_compute_intsum": {
"ail_rc_s": {
"baseline": 0.00044,
"baseline": 0.000362,
"tolerance_pct": 15
},
"ail_bump_s": {
"baseline": 0.000393,
"baseline": 0.000371,
"tolerance_pct": 15
},
"c_s": {
"baseline": 0.000374,
"baseline": 0.000361,
"tolerance_pct": 15
},
"rc_over_c": {
"baseline": 1.177715,
"baseline": 1.002676,
"tolerance_pct": 12
},
"bump_over_c": {
"baseline": 1.050856,
"baseline": 1.028255,
"tolerance_pct": 12
}
},
"bench_compute_collatz": {
"ail_rc_s": {
"baseline": 0.056878,
"baseline": 0.057994,
"tolerance_pct": 15
},
"ail_bump_s": {
"baseline": 0.056547,
"baseline": 0.057159,
"tolerance_pct": 15
},
"c_s": {
"baseline": 0.05748,
"baseline": 0.05776,
"tolerance_pct": 15
},
"rc_over_c": {
"baseline": 0.989523,
"baseline": 1.00405,
"tolerance_pct": 12
},
"bump_over_c": {
"baseline": 0.983756,
"baseline": 0.989593,
"tolerance_pct": 12
}
},
"bench_list_sum_explicit": {
"ail_rc_s": {
"baseline": 0.150754,
"tolerance_pct": 15
},
"ail_bump_s": {
"baseline": 0.0495,
"tolerance_pct": 15
},
"c_s": {
"baseline": 0.119204,
"tolerance_pct": 15
},
"rc_over_c": {
"baseline": 1.264667,
"tolerance_pct": 12
},
"bump_over_c": {
"baseline": 0.415253,
"tolerance_pct": 12
}
}
+5 -4
View File
@@ -47,10 +47,11 @@ OUTDIR = ROOT / "target" / "cross_lang"
# (ailang fixture stem, C reference stem) — both must exist as
# `examples/<ail>.ail.json` and `bench/reference/<c>.c`.
CORPUS = [
("bench_list_sum", "list_sum"),
("bench_tree_walk", "tree_walk"),
("bench_compute_intsum", "compute_intsum"),
("bench_compute_collatz", "compute_collatz"),
("bench_list_sum", "list_sum"),
("bench_tree_walk", "tree_walk"),
("bench_compute_intsum", "compute_intsum"),
("bench_compute_collatz", "compute_collatz"),
("bench_list_sum_explicit", "list_sum_explicit_free"),
]
+76
View File
@@ -0,0 +1,76 @@
// Hand-C reference for bench_list_sum_explicit — full malloc + free.
//
// Companion to list_sum.c (which leaks); this version adds explicit
// `free()` calls to mirror AILang's explicit-mode RC dec-tax. Pairs
// with examples/bench_list_sum_explicit.ailx via bench/cross_lang.py.
//
// The free pass walks the list after sum, freeing each cell. This is
// genuinely O(N) work that bench_list_sum.c never paid; the rc/c
// ratio for this fixture is therefore the honest answer to "how
// much does RC's dec-and-free pipeline cost vs hand-C's malloc-free
// pipeline on the same workload?".
//
// Note: we sum first, then free, rather than fold-with-free. This
// matches the AILang fixture's structure: sum_list owns the chain
// and consumes it; the dec walk happens implicitly as sum_acc
// pattern-matches LCons and re-binds the tail to the recursive call.
// AILang's pattern there is "consume during sum" — codegen emits
// dec on the consumed cells as the match arm closes. The C version
// could in principle interleave free into the sum loop too; doing it
// in two passes is structurally cleaner and the cost is the same
// (one O(N) walk + one O(N) walk-and-free).
//
// Build: clang -O2 -o list_sum_explicit_free list_sum_explicit_free.c
// Expected stdout (one int per line):
// 4999950000
// 499999500000
// 4499998500000
#include <stdio.h>
#include <stdlib.h>
typedef struct cell {
long head;
struct cell *tail;
} cell_t;
static cell_t *cons_n(long n) {
cell_t *acc = NULL;
for (long i = n - 1; i >= 0; i--) {
cell_t *c = (cell_t *) malloc(sizeof(cell_t));
c->head = i;
c->tail = acc;
acc = c;
}
return acc;
}
static long sum_list(const cell_t *xs) {
long acc = 0;
while (xs) {
acc += xs->head;
xs = xs->tail;
}
return acc;
}
static void free_list(cell_t *xs) {
while (xs) {
cell_t *next = xs->tail;
free(xs);
xs = next;
}
}
static void run_one(long n) {
cell_t *xs = cons_n(n);
printf("%ld\n", sum_list(xs));
free_list(xs);
}
int main(void) {
run_one(100000);
run_one(1000000);
run_one(3000000);
return 0;
}
+1 -1
View File
@@ -61,7 +61,7 @@ mkdir -p "$OUTDIR"
# Compile both modes for both fixtures up front so the bench loop only
# measures runtime, not build time.
fixtures=(bench_list_sum bench_tree_walk bench_closure_chain bench_hof_pipeline bench_compute_collatz)
fixtures=(bench_list_sum bench_tree_walk bench_closure_chain bench_hof_pipeline bench_compute_collatz bench_list_sum_explicit)
modes=(gc bump rc)
echo ">>> compiling fixtures (-O2)"
for f in "${fixtures[@]}"; do