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.
51 lines
1.8 KiB
Text
51 lines
1.8 KiB
Text
;;; 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)
|