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
213 lines
6.6 KiB
C
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;
|
|
}
|