lumbda/c/bench.c
russell@unturf.com b841b30bc4
c: precise GC tracing for NaN-boxed Values
Boehm's conservative pointer scan cannot recognize lumbda's Value
layout — heap pointers live in the low 48 bits with QNAN + tag bits
in the upper mantissa, so a raw word never looks like a heap address.
Until now main.c neutralized this with GC_disable(): every allocation
leaked, OOMing any long-running workload.

Add precise tracing via a custom Boehm kind:

- New c/gc.c: mark proc walks 8-byte words in mixed mode — when the
  QNAN bits are set with a pointer-bearing tag (0/2/4/5/6) extract
  the low-48 pointer; otherwise fall through to raw-pointer
  validation. GC_set_push_other_roots callback decodes NaN-boxed
  Values on the C stack via setjmp anchor + scan up to the stack
  base captured at process start.

- Allocations holding Values (Pair, Env bindings, ValueStack data,
  ULVector data, HTEntry, Proc params + body, FullCont stack,
  CodeObj instrs, SymbolEntry) route through lumbda_value_malloc.
  Pure-byte sites (bignum limbs, char buffers, source files) stay
  on regular GC_MALLOC.

- main.c / test.c / bench.c capture stack-base then drop GC_disable.

types.c also zeros popped slots on the value stack so stale pointers
do not survive a vs_pop and pin freed objects — independent
correctness fix that pays off once GC actually runs.

Build: USE_GC=1 (default when /usr/include/gc.h exists).

Tests with GC enabled:
- 88/88 c-test
- 4/4 regression-named-let-leak (test that motivated GC_disable)
- 205/205 functional (Python + C)
- zoe-favorites all tiers (Python + C + asm + asm-full)

alloc-test 1M cons drop-loop:
- Before: 0.60s wall, 156 MB RSS, leaks every cell
- After:  0.37s wall,   4 MB RSS, ~1500 GC cycles each freeing ~370 KB
2026-06-07 17:18:45 -04:00

213 lines
6.6 KiB
C

/*
* bench.c — Benchmarks: interpreted vs compiled vs JIT
*/
#include "lumbda.h"
#include "jit.h"
static double bench_time(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
typedef struct {
const char *name;
const char *setup;
const char *expr;
int iterations;
} Benchmark;
static Benchmark benchmarks[] = {
{
"fib-iter(30)",
"(define (fib-iter n)\n"
" (let loop ((a 0) (b 1) (i 0))\n"
" (if (= i n) a (loop b (+ a b) (+ i 1)))))\n",
"(fib-iter 30)",
10000
},
{
"fib-rec(25)",
"(define (fib-rec n)\n"
" (if (<= n 1) n (+ (fib-rec (- n 1)) (fib-rec (- n 2)))))\n",
"(fib-rec 25)",
10
},
{
"list-sum(1000)",
"(define (list-sum lst)\n"
" (let loop ((l lst) (acc 0))\n"
" (if (null? l) acc (loop (cdr l) (+ acc (car l))))))\n"
"(define big-list (iota 1000))\n",
"(list-sum big-list)",
1000
},
{
"map-square(1000)",
"(define (my-map f lst)\n"
" (if (null? lst) '()\n"
" (cons (f (car lst)) (my-map f (cdr lst)))))\n"
"(define big-list (iota 1000))\n",
"(my-map (lambda (x) (* x x)) big-list)",
100
},
{
"tak(18,12,6)",
"(define (tak x y z)\n"
" (if (not (< y x)) z\n"
" (tak (tak (- x 1) y z)\n"
" (tak (- y 1) z x)\n"
" (tak (- z 1) x y))))\n",
"(tak 18 12 6)",
10
},
{
"ack(3,4)",
"(define (ack m n)\n"
" (if (= m 0) (+ n 1)\n"
" (if (= n 0) (ack (- m 1) 1)\n"
" (ack (- m 1) (ack m (- n 1))))))\n",
"(ack 3 4)",
100
},
{
"ack(3,7)",
"(define (ack m n)\n"
" (if (= m 0) (+ n 1)\n"
" (if (= n 0) (ack (- m 1) 1)\n"
" (ack (- m 1) (ack m (- n 1))))))\n",
"(ack 3 7)",
10
},
{
"eml-compose(1M)",
/* EML insight (arXiv:2603.21852v2): eml(x,y) = exp(x) - ln(y) generates\n"
* ALL elementary functions from one operator. This benchmark exercises\n"
* integer-only EML-style composition: repeated add/sub (exp/ln analog\n"
* in the integer domain). Tests whether JIT compiles well the pattern\n"
* of composing minimal primitives into higher operations. */
"(define (eml-pow base exp)\n"
" (let loop ((e exp) (acc 1))\n"
" (if (= e 0) acc (loop (- e 1) (* acc base)))))\n"
"(define (eml-log-approx n base)\n"
" (let loop ((n n) (count 0))\n"
" (if (< n base) count (loop (- n base) (+ count 1)))))\n"
"(define (eml-compose x y)\n"
" (let ((p (eml-pow x y)))\n"
" (eml-log-approx p x)))\n",
"(eml-compose 3 7)",
100000
},
{
"named-let-sum(100k)",
/* Named-let compiles to a native loop with jmp — zero overhead. */
"(define (sum-to n)\n"
" (let loop ((i n) (acc 0))\n"
" (if (= i 0) acc (loop (- i 1) (+ acc i)))))\n",
"(sum-to 100000)",
100
},
{
"list-walk(1000)",
/* Tests JIT car/cdr/null?/cons pipeline */
"(define (list-reverse lst)\n"
" (let loop ((l lst) (acc '()))\n"
" (if (null? l) acc (loop (cdr l) (cons (car l) acc)))))\n"
"(define big-list (iota 1000))\n",
"(list-reverse big-list)",
1000
},
{NULL, NULL, NULL, 0}
};
static void run_benchmark(Benchmark *b, const char *mode) {
Env *g = make_global_env();
int count;
Value *prelude_exprs = read_all(PRELUDE, &count, false);
for (int i = 0; i < count; i++) leval(prelude_exprs[i], g);
ul_free(prelude_exprs);
g_auto_compile = false;
g_jit_enabled = false;
if (strcmp(mode, "compiled") == 0) {
g_auto_compile = true;
} else if (strcmp(mode, "jit") == 0) {
g_jit_enabled = true;
}
/* Setup */
Value *setup_exprs = read_all(b->setup, &count, false);
for (int i = 0; i < count; i++) leval(setup_exprs[i], g);
ul_free(setup_exprs);
/* Parse the benchmark expression once */
Value *bench_exprs = read_all(b->expr, &count, false);
/* Warmup */
for (int i = 0; i < 3; i++) {
for (int j = 0; j < count; j++) leval(bench_exprs[j], g);
}
/* Timed run */
double start = bench_time();
Value last = VAL_VOID;
for (int i = 0; i < b->iterations; i++) {
for (int j = 0; j < count; j++) last = leval(bench_exprs[j], g);
}
double elapsed = bench_time() - start;
char *result_str = show(last, false);
printf(" %-25s %-10s %8d iters %.3f s (%.1f us/iter) => %s\n",
b->name, mode,
b->iterations, elapsed,
(elapsed / b->iterations) * 1e6,
result_str);
ul_free(result_str);
ul_free(bench_exprs);
g_auto_compile = false;
g_jit_enabled = false;
}
int main(void) {
int stack_anchor;
#ifdef USE_BOEHM_GC
GC_INIT();
lumbda_gc_init();
lumbda_gc_set_stack_base(&stack_anchor);
#else
(void)stack_anchor;
#endif
init_symbols();
/* Set up root error context for prelude loading */
ErrorContext root_ctx;
root_ctx.call_stack_depth = 0;
root_ctx.error_obj = VAL_NIL;
root_ctx.source_line = 0;
g_error_ctx = &root_ctx;
if (setjmp(root_ctx.jmp) != 0) {
fprintf(stderr, "fatal error: %s\n", root_ctx.message);
return 1;
}
printf("lumbda C benchmarks — interpreted vs compiled vs JIT\n");
printf("═════════════════════════════════════════════════════════════════\n");
const char *modes[] = { "interp", "compiled", "jit", NULL };
for (int i = 0; benchmarks[i].name; i++) {
for (int m = 0; modes[m]; m++) {
TRY(ctx) {
run_benchmark(&benchmarks[i], modes[m]);
} CATCH {
printf(" %-25s %-10s ERROR: %s\n", benchmarks[i].name, modes[m], ctx.message);
} ENDTRY;
}
printf(" ─────────────────────────────────────────────────────────────\n");
}
printf("═════════════════════════════════════════════════════════════════\n");
return 0;
}