lumbda/examples/bench-gc-memory.lsp
russell@unturf.com 489776baa4 asm: naive stop-the-world mark-sweep GC as a control group
Adds a second asm build (asm/uncommonlisp-gc) behind the GC_NAIVE
assembler flag, providing the benchmark baseline we previously had
no data for. Same binary, same surface, different allocator:

  - 8-byte header per heap block (size << 1 | mark), placed at -8
    from the tagged pointer so existing untag + offset accesses
    stay unchanged.
  - Chunk list tracked in a side array, letting sweep walk every
    mmap'd region by header-chained blocks instead of guessing.
  - Free list rebuilt each sweep, first-fit alloc with split on
    large-leftover (>= 24 bytes).
  - Mark phase enumerates five root classes: %r14 (global env,
    untagged chain), sym_else_val, sym_table entries, every
    sym_hash_bucket chain, and a conservative scan from current
    %rsp to the initial stack_top captured at _start. The stack
    scan runs twice per word — once as a tagged value, once as a
    potential untagged env-node pointer (size-guarded to 24 bytes
    so it can't walk off a wrong-size block).
  - Transitive marking via an explicit 16K-entry mark stack;
    gc_mark_env walks untagged env chains from %r14 and from every
    closure's env field.
  - heap_alloc preserves the non-GC ABI (only %rax clobbered) so
    existing callers like bi_append, which holds state in %rcx
    across make_pair, keep working.
  - Overflow path uses check-then-write bumps and pads the old
    chunk's tail with a single dead block before growing, so sweep
    never walks into uninitialized mmap'd memory.
  - HEAP_SIZE shrinks to 1 MB under GC_NAIVE so the collector
    actually runs on ordinary workloads.
  - Two diagnostic builtins in the GC build: (gc-collect) to force
    a collection, (gc-stats) -> (collections . live-bytes).

Control-group bench (examples/bench-gc-memory.lsp, 2000 iterations
of build-sum-discard over 200-element lists, i5-8350U):

  tier           time_ms   peak_rss   final_rss
  asm no-GC       1097     133.9 MB   133.9 MB   (grows, never shrinks)
  asm naive GC    1431       1.1 MB     1.1 MB   (steady state)

124x less memory at a ~30% throughput cost. That is the number we
were guessing at before. Reproduce: make bench-gc.

Tests: 137 asm (no-GC) + 137 asm (GC) + 189 shared functional pass.
The two asm builds are tested independently via UNCOMMONLISP_BIN in
asm/test.sh; asm/Makefile now builds both and exposes a test-gc
target.
2026-04-18 09:34:51 -04:00

33 lines
1.1 KiB
Text

;;; bench-gc-memory.lsp — sustained allocation workload that exposes
;;; the bump-only tier's unbounded growth vs the naive GC tier's
;;; bounded steady-state. The body builds a throwaway list of K pairs
;;; and sums it, repeated N times. Each iteration's list is
;;; unreachable after the sum, so a collecting heap stays flat while
;;; a bump-only heap grows linearly.
;;;
;;; Wall-clock timing is reported from inside; peak RSS is sampled
;;; by the wrapper (tests/bench-gc-memory.sh).
(define K 200)
(define N 2000)
(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 total 0)
(define i 0)
(define (step)
(if (>= i N) total
(begin
(set! total (+ total (sum-list (build-list K '()) 0)))
(set! i (+ i 1))
(step))))
(define t0 (current-time-ms))
(display "workload N=") (display N) (display " K=") (display K) (newline)
(display "checksum=") (display (step)) (newline)
(display "time_ms=") (display (- (current-time-ms) t0)) (newline)