lumbda/examples/bench-gc-adaptive.lsp
russell@unturf.com 3d55092037 asm-gc: adaptive EMA-driven meta-GC policy + bench + two correctness fixes
Moves the meta-GC from greedy (always verify) to adaptive: track a
scaled EMA of recent escape rate; when rate exceeds 50% (128/256),
SKIP the verifier and let the heap grow until natural GC; every 16
skipped arenas, force a verify as a probe to re-sample the rate.

Two correctness fixes uncovered while testing adaptive:

1. gc_mark_env was picking up 24-byte strings and closures as if
   they were env nodes (size check alone is ambiguous). Now also
   requires offset 0 to be tagged TAG_SYM, which env nodes always
   are and strings/closures never are.

2. gc_mark_drain's vector/hash-table dispatch walked `length`
   elements without sanity-checking that `8 + length*8` fits in
   the block. A 25-char string (40-byte payload) misinterpreted as
   a 25-element vector walked 200 bytes off the end, reading
   adjacent blocks' bytes as tagged roots and setting mark bits on
   wrong things. Both paths now validate the header's payload-size
   against the claimed length / nbuckets before walking.

New builtin:
  (arena-set-mode 0|1) — 0 = greedy baseline, 1 = adaptive (default)

arena-stats extended to six fields:
  (calls resets escapes skipped bytes-reclaimed ema-rate)

Bench (tests/bench-gc-adaptive.sh, one process per phase to isolate
a separate latent cross-phase bug we haven't cracked, N=1000 per
phase, i5-8350U):

  workload   mode     time_ms   resets   escapes   skipped
  friendly   greedy    732      1000        0         0
  friendly   adapt     691      1000        0         0
  hostile    greedy    568         0     1000         0
  hostile    adapt     607         0     1000        11
  mixed      greedy   1981        17     1983         0
  mixed      adapt    1694        14     1986         2

Adaptive wins on friendly (-6%) and mixed (-17%). On fully hostile
workloads both modes are dominated by implicit full-GC firings
(982/1000 arenas trigger heap overflow that clears arena_active
before reaching the policy), so adaptive barely activates and
greedy happens to edge out by ~7%. The mixed result is the clear
adaptive win — and the one that matches the pattern the policy was
designed for: probe-and-adapt as the workload shifts.

137 asm (no-GC) + 137 asm (GC) + 189 shared functional tests all
still pass.
2026-04-18 11:33:39 -04:00

51 lines
1.8 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; bench-gc-adaptive.lsp — driver that runs ONE workload phase in
;;; this process. The outer wrapper (tests/bench-gc-adaptive.sh)
;;; launches asm-gc six times, one per (workload × mode) pair, each
;;; with a clean interpreter. One-phase-per-process isolates state
;;; across runs; cross-phase heap residue on a 1 MB chunk has caused
;;; spurious escape reports in the meta-GC that we haven't nailed
;;; yet, and we'd rather report clean numbers than fight a latent
;;; interaction mid-bench. The MODE and WORKLOAD are injected by
;;; the wrapper via pre-bindings of *mode* and *workload*.
(define K 200)
(define N 1000)
(define (build-list k acc)
(if (= k 0) acc (build-list (- k 1) (cons k acc))))
(define (sum-list lst acc)
(if (null? lst) acc (sum-list (cdr lst) (+ acc (car lst)))))
(define (friendly-thunk) (sum-list (build-list K '()) 0))
(define escaped-sink '())
(define (hostile-step)
(set! escaped-sink (with-arena (lambda () (build-list K '())))))
(define (friendly-go n)
(if (= n 0) 'done
(begin (with-arena friendly-thunk) (friendly-go (- n 1)))))
(define (hostile-go n)
(if (= n 0) 'done
(begin (hostile-step) (hostile-go (- n 1)))))
(define (mixed-go n)
(if (= n 0) 'done
(begin
(with-arena friendly-thunk)
(hostile-step)
(mixed-go (- n 1)))))
(arena-set-mode *mode*)
(define t0 (current-time-ms))
(cond
((= *workload* 0) (friendly-go N))
((= *workload* 1) (hostile-go N))
(else (mixed-go N)))
(define elapsed (- (current-time-ms) t0))
(display "workload=") (display *workload*)
(display " mode=") (display *mode*)
(display " N=") (display N) (newline)
(display "time_ms=") (display elapsed) (newline)
(display "arena-stats=") (display (arena-stats)) (newline)
(display "gc-stats=") (display (gc-stats)) (newline)