From 489776baa4e8a00efbb71518c0b186ebaeb2c373 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sat, 18 Apr 2026 09:34:51 -0400 Subject: [PATCH] asm: naive stop-the-world mark-sweep GC as a control group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 3 + asm/Makefile | 15 +- asm/test.sh | 2 +- asm/uncommonlisp-gc | Bin 0 -> 57400 bytes asm/uncommonlisp-gc.o | Bin 0 -> 57720 bytes asm/uncommonlisp.s | 678 +++++++++++++++++++++++++++++++++++ examples/bench-gc-memory.lsp | 33 ++ tests/bench-gc-memory.sh | 46 +++ 8 files changed, 772 insertions(+), 5 deletions(-) create mode 100755 asm/uncommonlisp-gc create mode 100644 asm/uncommonlisp-gc.o create mode 100644 examples/bench-gc-memory.lsp create mode 100755 tests/bench-gc-memory.sh diff --git a/Makefile b/Makefile index a2ab9d5..9625e89 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,9 @@ bench-proof: c-build asm-build bench-hashset: asm-build @bash tests/bench-hashset.sh +bench-gc: asm-build + @bash tests/bench-gc-memory.sh + bench-all: bench c-bench bench-3way bench-portal bench-portal-cross bench-web bench-rpc-chain bench-proof @echo "═══════════════════════════════════════════════════════════" @echo "All benchmarks complete. Numbers in the whitepaper §6.4," diff --git a/asm/Makefile b/asm/Makefile index 69dcf75..60e6647 100644 --- a/asm/Makefile +++ b/asm/Makefile @@ -1,13 +1,20 @@ -all: uncommonlisp +all: uncommonlisp uncommonlisp-gc uncommonlisp: uncommonlisp.s as --64 -o uncommonlisp.o uncommonlisp.s ld -o uncommonlisp uncommonlisp.o +uncommonlisp-gc: uncommonlisp.s + as --64 --defsym GC_NAIVE=1 -o uncommonlisp-gc.o uncommonlisp.s + ld -o uncommonlisp-gc uncommonlisp-gc.o + test: uncommonlisp @bash test.sh -clean: - rm -f uncommonlisp.o uncommonlisp +test-gc: uncommonlisp-gc + @UNCOMMONLISP_BIN=./uncommonlisp-gc bash test.sh -.PHONY: all test clean +clean: + rm -f uncommonlisp.o uncommonlisp uncommonlisp-gc.o uncommonlisp-gc + +.PHONY: all test test-gc clean diff --git a/asm/test.sh b/asm/test.sh index 63dd214..9b5d9f1 100644 --- a/asm/test.sh +++ b/asm/test.sh @@ -10,7 +10,7 @@ cd "$(dirname "$0")" PASS=0 FAIL=0 -UL=./uncommonlisp +UL="${UNCOMMONLISP_BIN:-./uncommonlisp}" check() { local name="$1" input="$2" expected="$3" diff --git a/asm/uncommonlisp-gc b/asm/uncommonlisp-gc new file mode 100755 index 0000000000000000000000000000000000000000..69aa4de4a2bf27a2d14c97f7186355fe8b62bab0 GIT binary patch literal 57400 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 bOO|I@v-!lA@-gN1bt%6GzvLN5 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 z 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 literal 0 HcmV?d00001 diff --git a/asm/uncommonlisp.s b/asm/uncommonlisp.s index e3a1862..1d82b9a 100644 --- a/asm/uncommonlisp.s +++ b/asm/uncommonlisp.s @@ -74,7 +74,14 @@ # fd is recovered via (val >> 3) - PORT_SPECIAL_BASE. .equ PORT_SPECIAL_BASE, 1000 +.ifdef GC_NAIVE +# Smaller chunks in the GC build so the collector actually runs +# on ordinary workloads (otherwise nothing fills a 64 MB chunk +# fast enough to trigger mark-sweep within a single bench). +.equ HEAP_SIZE, 0x100000 # 1 MB +.else .equ HEAP_SIZE, 0x4000000 # 64 MB +.endif # Builtin indices .equ BI_ADD, 0 @@ -185,7 +192,13 @@ .equ BI_HS_HAS, 105 .equ BI_HS_SIZE, 106 .equ BI_HS_LIST, 107 +.ifdef GC_NAIVE +.equ BI_GC_COLLECT, 108 +.equ BI_GC_STATS, 109 +.equ BI_COUNT, 110 +.else .equ BI_COUNT, 108 +.endif # ============================================================ .data @@ -319,6 +332,10 @@ bn_hsadd: .byte 13; .ascii "hash-set-add!" bn_hshas: .byte 18; .ascii "hash-set-contains?" bn_hssize: .byte 13; .ascii "hash-set-size" bn_hslist: .byte 14; .ascii "hash-set->list" +.ifdef GC_NAIVE +bn_gccollect: .byte 10; .ascii "gc-collect" +bn_gcstats: .byte 8; .ascii "gc-stats" +.endif s_hashtable: .ascii "#" .equ s_hashtable_len, . - s_hashtable @@ -361,6 +378,9 @@ bi_names: .quad bn_htmake, bn_htp, bn_htset, bn_htref, bn_htrefd, bn_htdel .quad bn_htexists, bn_htsize, bn_htkeys, bn_htvals, bn_htalist .quad bn_hsmake, bn_hsp, bn_hsadd, bn_hshas, bn_hssize, bn_hslist +.ifdef GC_NAIVE + .quad bn_gccollect, bn_gcstats +.endif # Error messages err_unbound: .ascii "Error: unbound variable: " @@ -421,6 +441,29 @@ heap_base: .skip 8 .equ SYM_HASH_SIZE, 1024 sym_hash_buckets: .skip 8192 # 1024 * 8 bytes +.ifdef GC_NAIVE +# ─────────────────────────────────────────────────────────── +# Naive mark-and-sweep GC state (control group for bench). +# Every heap block carries an 8-byte header: (size << 1) | mark. +# The tagged pointer still points at the payload (header at -8). +# Chunks are tracked in a side array so chunk memory stays pure +# allocation space. Free blocks are linked via a global list +# whose nodes reuse the header word as size and the first 8 bytes +# of payload as the next-pointer. +# ─────────────────────────────────────────────────────────── +.equ GC_MAX_CHUNKS, 32 +.equ GC_MARK_STACK_CAP, 16384 # 16K tagged values; 128 KB +stack_top: .skip 8 # captured at _start +gc_chunk_base: .skip 256 # 32 * 8 +gc_chunk_end: .skip 256 # 32 * 8 +gc_chunk_count: .skip 8 +gc_free_list: .skip 8 # head pointer (or 0) +gc_mark_stack: .skip 131072 # 16K * 8 +gc_mark_depth: .skip 8 +gc_collections: .skip 8 # counter for --gc-stat +gc_live_bytes: .skip 8 # updated at end of sweep +.endif + # ============================================================ .text # ============================================================ @@ -430,6 +473,11 @@ sym_hash_buckets: .skip 8192 # 1024 * 8 bytes # _start: entry point # ============================================================ _start: +.ifdef GC_NAIVE + # Capture initial stack pointer before any push — stack_top is + # the upper bound of the conservative root scan. + movq %rsp, stack_top(%rip) +.endif # Allocate heap movq $SYS_MMAP, %rax xorq %rdi, %rdi @@ -442,6 +490,13 @@ _start: movq %rax, %r15 # heap pointer movq %rax, heap_base(%rip) # save base for portal leaq HEAP_SIZE(%r15), %r13 # heap limit +.ifdef GC_NAIVE + # Register this chunk. + movq %rax, gc_chunk_base(%rip) + leaq HEAP_SIZE(%rax), %rcx + movq %rcx, gc_chunk_end(%rip) + movq $1, gc_chunk_count(%rip) +.endif # Init global env = 0 (empty) xorq %r14, %r14 @@ -525,6 +580,7 @@ repl_exit: # heap_alloc: allocate %rdi bytes, return pointer in %rax # Bumps %r15. If out of space, mmap more. # ============================================================ +.ifndef GC_NAIVE heap_alloc: # Align size up to 8 addq $7, %rdi @@ -555,6 +611,622 @@ heap_alloc: movq %r15, %rax addq %rdi, %r15 ret +.endif + +.ifdef GC_NAIVE +# GC-flavored heap_alloc. +# Layout: every block is [header:8 | payload:size] +# header = (size << 1) | mark. Pointer returned = header+8. +# Try free list first-fit; else bump; if bump overflows, GC; if +# still no room, register a new chunk. +# +# ABI parity with the non-GC heap_alloc: preserves every register +# except %rax (return) and the %r15 bump pointer. Callers like +# bi_append hold state in %rcx across heap_alloc; we mustn't break +# that contract just because we added a GC path. +heap_alloc: + addq $7, %rdi + andq $-8, %rdi # aligned payload size + testq %rdi, %rdi + jnz 1f + movq $8, %rdi # minimum payload = 8 bytes (free-list next ptr) +1: + pushq %rbx + pushq %rcx + pushq %rdx + pushq %rsi + pushq %rbp + pushq %r8 + pushq %r9 + pushq %r10 + pushq %r11 + pushq %r12 + movq %rdi, %rbx # size (callee-save register for this function) + + movq %rbx, %rdi + call gc_freelist_alloc + testq %rax, %rax + jnz .ha_done + +.ha_bump: + # Check-then-write: only emit the header if the new block fits, + # so a rolled-back attempt never leaves a stale header that sweep + # would later walk through. + leaq 8(%rbx), %r12 # total bytes (header + payload) + movq %r15, %rcx + addq %r12, %rcx # proposed new %r15 + cmpq %r13, %rcx + ja .ha_overflow + movq %rbx, %rdx + shlq $1, %rdx # header = size << 1, mark=0 + movq %rdx, (%r15) + movq %r15, %rax + addq $8, %rax # payload ptr + movq %rcx, %r15 # commit bump +.ha_done: + popq %r12 + popq %r11 + popq %r10 + popq %r9 + popq %r8 + popq %rbp + popq %rsi + popq %rdx + popq %rcx + popq %rbx + ret + +.ha_overflow: + # Bump rejected (no commit). Run GC, retry free list, retry bump. + call gc_collect + movq %rbx, %rdi + call gc_freelist_alloc + testq %rax, %rax + jnz .ha_done + # Retry bump in current chunk (GC may have recovered nothing at + # the tail, but try once in case free list had pure fragments). + leaq 8(%rbx), %r12 + movq %r15, %rcx + addq %r12, %rcx + cmpq %r13, %rcx + ja .ha_grow + movq %rbx, %rdx + shlq $1, %rdx + movq %rdx, (%r15) + movq %r15, %rax + addq $8, %rax + movq %rcx, %r15 + jmp .ha_done + +.ha_grow: + # Before abandoning the current chunk, fill its remaining tail + # with a single dead block so the sweep walker has a coherent + # last-block sentinel. Without this, the gap between %r15 and + # the mmap end is garbage that sweep would misread as headers. + movq %r13, %rax + subq %r15, %rax # tail bytes remaining (>= 0) + cmpq $16, %rax + jb .ha_grow_no_pad # too small for header+min-payload; accept leak + movq %rax, %rdx + subq $8, %rdx # padding payload size + movq %rdx, %rcx + shlq $1, %rcx # header, mark=0 + movq %rcx, (%r15) + # Link onto free list directly so sweep doesn't need to know. + movq gc_free_list(%rip), %rcx + movq %rcx, 8(%r15) + movq %r15, gc_free_list(%rip) + addq %rax, %r15 # %r15 now == %r13 +.ha_grow_no_pad: + movq $SYS_MMAP, %rax + xorq %rdi, %rdi + movq $HEAP_SIZE, %rsi + movq $3, %rdx + movq $0x22, %r10 + movq $-1, %r8 + xorq %r9, %r9 + syscall + cmpq $-1, %rax + je die_oom + movq %rax, %r15 + leaq HEAP_SIZE(%rax), %r13 + movq gc_chunk_count(%rip), %rdx + cmpq $GC_MAX_CHUNKS, %rdx + jae die_oom + leaq gc_chunk_base(%rip), %rcx + movq %rax, (%rcx,%rdx,8) + leaq gc_chunk_end(%rip), %rcx + movq %r13, (%rcx,%rdx,8) + incq %rdx + movq %rdx, gc_chunk_count(%rip) + jmp .ha_bump + +# gc_freelist_alloc(%rdi=size) -> %rax = payload ptr or 0 if none fits +# First-fit scan. Splits large blocks if the leftover >= 24 bytes +# (header + min payload). Caller has aligned %rdi to 8. +gc_freelist_alloc: + movq gc_free_list(%rip), %rax # cursor (header addr) + xorq %rcx, %rcx # prev (0) +.gfa_scan: + testq %rax, %rax + jz .gfa_empty + movq (%rax), %rdx # header + shrq $1, %rdx # block payload size + cmpq %rdi, %rdx + jb .gfa_next + # Fits. Unlink. + movq 8(%rax), %rsi # next free + testq %rcx, %rcx + jnz 1f + movq %rsi, gc_free_list(%rip) + jmp 2f +1: + movq %rsi, 8(%rcx) +2: + # If leftover space is >= 24, split (keep the remainder on the list). + movq %rdx, %r8 + subq %rdi, %r8 # leftover payload bytes + cmpq $24, %r8 + jb .gfa_return_whole + # Split: new tail free block at %rax + 8 + %rdi. + # tail header size = r8 - 8 (one of the leftover bytes becomes the tail header) + leaq 8(%rax,%rdi), %r9 # tail header addr + subq $8, %r8 + movq %r8, %r10 + shlq $1, %r10 # mark=0 + movq %r10, (%r9) + movq gc_free_list(%rip), %r11 + movq %r11, 8(%r9) + movq %r9, gc_free_list(%rip) + # Shrink current block's header. + movq %rdi, %rdx + shlq $1, %rdx + movq %rdx, (%rax) +.gfa_return_whole: + addq $8, %rax # payload ptr + ret +.gfa_next: + movq %rax, %rcx + movq 8(%rax), %rax + jmp .gfa_scan +.gfa_empty: + xorq %rax, %rax + ret + +# gc_collect: stop-the-world mark-and-sweep. +# Saves all caller registers on stack, scans: +# 1. %r14 global env (tagged) +# 2. sym_else_val (tagged) +# 3. sym_table entries (untagged symbol storage) +# 4. sym_hash_buckets chains (untagged chain nodes + their sym_ptrs) +# 5. stack words from %rsp to stack_top (conservative: tag check + chunk range check) +# Marks transitively via an explicit mark stack. Then sweeps every +# chunk linearly using each block's size-header and rebuilds the +# free list. +gc_collect: + # Save all general-purpose registers so the stack scan catches + # tagged values that were live in registers at GC entry. + pushq %rax + pushq %rbx + pushq %rcx + pushq %rdx + pushq %rsi + pushq %rdi + pushq %rbp + pushq %r8 + pushq %r9 + pushq %r10 + pushq %r11 + pushq %r12 + + # Clear mark stack. + movq $0, gc_mark_depth(%rip) + + # Root 1: global env. Env nodes are UNTAGGED 24-byte triples + # (sym, val, parent_untagged), so we walk them with the dedicated + # env walker rather than the tagged-pointer push. + movq %r14, %rdi + call gc_mark_env + + # Root 2: cached else sym. + movq sym_else_val(%rip), %rdi + call gc_push_if_heap + + # Root 3: sym_table (untagged symbol storage pointers). + movq sym_count(%rip), %rcx + leaq sym_table(%rip), %rsi +.gcc_r_sym: + testq %rcx, %rcx + jz .gcc_r_sym_done + pushq %rcx + pushq %rsi + movq (%rsi), %rdi + testq %rdi, %rdi + jz 1f + call gc_mark_untagged +1: + popq %rsi + popq %rcx + addq $8, %rsi + decq %rcx + jmp .gcc_r_sym +.gcc_r_sym_done: + + # Root 4: sym_hash_buckets — each bucket head is an untagged + # chain-node pointer; chain nodes are [sym_ptr:8][next:8], all + # allocated via heap_alloc so they carry GC headers. Walk each + # chain and mark both the node and the symbol it references. + movq $SYM_HASH_SIZE, %rcx + leaq sym_hash_buckets(%rip), %rsi +.gcc_r_bkt: + testq %rcx, %rcx + jz .gcc_r_bkt_done + movq (%rsi), %rdi # bucket head (untagged, or 0) +.gcc_r_chain: + testq %rdi, %rdi + jz .gcc_r_bkt_next + pushq %rcx + pushq %rsi + pushq %rdi + call gc_mark_untagged # mark chain node's header + popq %rdi + movq (%rdi), %r8 # sym_ptr (untagged) + pushq %rdi + testq %r8, %r8 + jz 2f + movq %r8, %rdi + call gc_mark_untagged +2: + popq %rdi + movq 8(%rdi), %rdi # next chain node + popq %rsi + popq %rcx + jmp .gcc_r_chain +.gcc_r_bkt_next: + addq $8, %rsi + decq %rcx + jmp .gcc_r_bkt +.gcc_r_bkt_done: + + # Root 5: conservative stack scan. Each word gets two tries: + # (a) treat as tagged value → gc_push_if_heap; (b) treat as an + # untagged env-node pointer → gc_mark_env (size-guarded). (b) + # catches the live %rbp (current local env) which the tagged + # path skips because env nodes look like TAG_INT. + movq %rsp, %rsi + movq stack_top(%rip), %rdx +.gcc_r_stk: + cmpq %rdx, %rsi + jae .gcc_r_stk_done + pushq %rsi + pushq %rdx + movq (%rsi), %rdi + call gc_push_if_heap + popq %rdx + popq %rsi + pushq %rsi + pushq %rdx + movq (%rsi), %rdi + call gc_mark_env + popq %rdx + popq %rsi + addq $8, %rsi + jmp .gcc_r_stk +.gcc_r_stk_done: + + # Drain mark stack: each entry is a tagged value whose header + # has NOT yet been marked. Pop, mark, recurse by tag. + call gc_mark_drain + + # Sweep. + call gc_sweep + + # Stats. + incq gc_collections(%rip) + + # Restore registers. + popq %r12 + popq %r11 + popq %r10 + popq %r9 + popq %r8 + popq %rbp + popq %rdi + popq %rsi + popq %rdx + popq %rcx + popq %rbx + popq %rax + ret + +# gc_mark_untagged: %rdi = untagged heap ptr. Set mark bit in +# the header (at ptr-8). No recursion — used for chain nodes and +# symbol storage which don't contain tagged pointers. +gc_mark_untagged: + testq %rdi, %rdi + jz .gmu_end + # Ensure ptr lives in a chunk. + call gc_ptr_in_chunk + testq %rax, %rax + jz .gmu_end + orq $1, -8(%rdi) +.gmu_end: + ret + +# gc_mark_env: %rdi = untagged env node pointer (24-byte triple +# [sym, val, parent_untagged]) — walks the parent chain, marking +# each node's header and pushing (sym, val) to the mark stack for +# transitive tagged-value marking. Size-guarded: the walk stops if +# a node's header size isn't 24, so the scan can be called on any +# word (closure env, stack-resident %rbp, global %r14) without +# fear of walking off a wrong-sized block. +gc_mark_env: + testq %rdi, %rdi + jz .gme_end + # Must be 8-byte aligned and in a chunk. + testq $7, %rdi + jnz .gme_end + pushq %rdi + call gc_ptr_in_chunk + movq %rax, %rcx + popq %rdi + testq %rcx, %rcx + jz .gme_end + # Block's header payload-size must be 24 (our env-node sentinel). + movq -8(%rdi), %rax + shrq $1, %rax + cmpq $24, %rax + jne .gme_end + # Already marked? Skip. + testq $1, -8(%rdi) + jnz .gme_end + orq $1, -8(%rdi) + # Push sym and val as tagged values. + pushq %rdi + movq (%rdi), %rdi + call gc_push_if_heap + movq (%rsp), %rdi + movq 8(%rdi), %rdi + call gc_push_if_heap + popq %rdi + # Tail-walk parent. + movq 16(%rdi), %rdi + jmp gc_mark_env +.gme_end: + ret + +# gc_ptr_in_chunk: %rdi = raw ptr. Returns %rax != 0 if ptr falls +# inside a registered chunk's allocation range, else 0. +gc_ptr_in_chunk: + movq gc_chunk_count(%rip), %rcx + xorq %r8, %r8 + leaq gc_chunk_base(%rip), %r9 + leaq gc_chunk_end(%rip), %r10 +.gpc_loop: + cmpq %rcx, %r8 + jae .gpc_no + movq (%r9,%r8,8), %rdx # base + cmpq %rdx, %rdi + jb .gpc_next + movq (%r10,%r8,8), %rdx # end + cmpq %rdx, %rdi + jae .gpc_next + movq $1, %rax + ret +.gpc_next: + incq %r8 + jmp .gpc_loop +.gpc_no: + xorq %rax, %rax + ret + +# gc_push_if_heap: %rdi = possibly-tagged value. If it looks like +# a heap-pointing tagged value AND points into a chunk AND its +# header is not yet marked, push onto the mark stack. +gc_push_if_heap: + movq %rdi, %rax + andq $TAG_MASK, %rax + # Heap-pointing tags: PAIR(1), SYM(2), CLOSURE(3), STRING(6), 7(vector/ht/hs). + cmpq $TAG_PAIR, %rax + je .gpih_try + cmpq $TAG_SYM, %rax + je .gpih_try + cmpq $TAG_CLOSURE, %rax + je .gpih_try + cmpq $TAG_STRING, %rax + je .gpih_try + cmpq $7, %rax + je .gpih_try + ret +.gpih_try: + movq %rdi, %rsi + andq $-8, %rsi # untagged ptr + pushq %rdi + movq %rsi, %rdi + call gc_ptr_in_chunk + movq %rax, %rcx + popq %rdi + testq %rcx, %rcx + jz .gpih_end + movq %rdi, %rsi + andq $-8, %rsi + testq $1, -8(%rsi) + jnz .gpih_end # already marked + # Push. + movq gc_mark_depth(%rip), %rcx + cmpq $GC_MARK_STACK_CAP, %rcx + jae .gpih_end # silently drop on overflow — correctness preserved + # (sweep won't reclaim missed-roots, just leaks one cycle) + leaq gc_mark_stack(%rip), %rsi + movq %rdi, (%rsi,%rcx,8) + incq %rcx + movq %rcx, gc_mark_depth(%rip) +.gpih_end: + ret + +# gc_mark_drain: pop tagged values from mark stack, set header +# mark, push children based on tag. Loop until empty. +gc_mark_drain: +.gmd_top: + movq gc_mark_depth(%rip), %rcx + testq %rcx, %rcx + jz .gmd_done + decq %rcx + movq %rcx, gc_mark_depth(%rip) + leaq gc_mark_stack(%rip), %rsi + movq (%rsi,%rcx,8), %rbx # tagged value (callee-save keeps it safe) + movq %rbx, %rsi + andq $-8, %rsi # untagged ptr + # If already marked, skip. + testq $1, -8(%rsi) + jnz .gmd_top + orq $1, -8(%rsi) # mark header + # Dispatch by tag. + movq %rbx, %rax + andq $TAG_MASK, %rax + cmpq $TAG_PAIR, %rax + je .gmd_pair + cmpq $TAG_CLOSURE, %rax + je .gmd_closure + cmpq $7, %rax + je .gmd_vec7 + # STRING / SYM: no children. + jmp .gmd_top +.gmd_pair: + movq (%rsi), %rdi + call gc_push_if_heap + movq %rbx, %rsi + andq $-8, %rsi + movq 8(%rsi), %rdi + call gc_push_if_heap + jmp .gmd_top +.gmd_closure: + # [params:tagged | body:tagged | env:untagged] + movq (%rsi), %rdi + call gc_push_if_heap + movq %rbx, %rsi + andq $-8, %rsi + movq 8(%rsi), %rdi + call gc_push_if_heap + movq %rbx, %rsi + andq $-8, %rsi + movq 16(%rsi), %rdi + call gc_mark_env # env field is an untagged env chain + jmp .gmd_top +.gmd_vec7: + movq (%rsi), %rdx # first word: length or -1 or -2 + testq %rdx, %rdx + jns .gmd_vector_body + # Hash-table (-1) or hash-set (-2): buckets 0..nbuckets-1 at offset 24. + movq 16(%rsi), %rcx # nbuckets + xorq %r8, %r8 +.gmd_ht_loop: + cmpq %rcx, %r8 + jae .gmd_top + pushq %rcx + pushq %r8 + pushq %rsi + movq 24(%rsi,%r8,8), %rdi + call gc_push_if_heap + popq %rsi + popq %r8 + popq %rcx + incq %r8 + jmp .gmd_ht_loop +.gmd_vector_body: + # Vector: length = rdx; elements at offset 8. + xorq %r8, %r8 +.gmd_vec_loop: + cmpq %rdx, %r8 + jae .gmd_top + pushq %rdx + pushq %r8 + pushq %rsi + movq 8(%rsi,%r8,8), %rdi + call gc_push_if_heap + popq %rsi + popq %r8 + popq %rdx + incq %r8 + jmp .gmd_vec_loop +.gmd_done: + ret + +# gc_sweep: walk every chunk linearly, reclaim unmarked blocks +# onto the free list, clear mark bits on live blocks. Rebuilds +# the free list from scratch each sweep (no carry-over; simpler). +gc_sweep: + movq $0, gc_free_list(%rip) + movq $0, gc_live_bytes(%rip) + movq gc_chunk_count(%rip), %rcx + xorq %r8, %r8 # chunk index + leaq gc_chunk_base(%rip), %r9 + leaq gc_chunk_end(%rip), %r10 +.gsw_chunk: + cmpq %rcx, %r8 + jae .gsw_done + movq (%r9,%r8,8), %rbx # cursor at chunk base + # If this is the current (last) chunk, walk up to %r15; else up to chunk end. + movq %rcx, %rdx + decq %rdx + cmpq %rdx, %r8 + je .gsw_use_r15 + movq (%r10,%r8,8), %rdi + jmp .gsw_walk +.gsw_use_r15: + movq %r15, %rdi +.gsw_walk: + cmpq %rdi, %rbx + jae .gsw_next_chunk + movq (%rbx), %rsi # header + movq %rsi, %r11 + shrq $1, %r11 # payload size + testq $1, %rsi + jz .gsw_dead + # Live: clear mark. + andq $-2, %rsi + movq %rsi, (%rbx) + addq %r11, gc_live_bytes(%rip) + leaq 8(%rbx,%r11), %rbx + jmp .gsw_walk +.gsw_dead: + # Dead: keep existing header (size, mark=0 already), link into free list. + movq gc_free_list(%rip), %rdx + movq %rdx, 8(%rbx) # next ptr in payload[0] + movq %rbx, gc_free_list(%rip) + leaq 8(%rbx,%r11), %rbx + jmp .gsw_walk +.gsw_next_chunk: + incq %r8 + jmp .gsw_chunk +.gsw_done: + ret + +# bi_gc_collect_user: (gc-collect) -> void. Forces a collection. +# Uses inline popq sequence since this sits above the RET_VAL macro. +bi_gc_collect_user: + call gc_collect + movq $VAL_VOID, %rax + popq %r12 + popq %rbp + popq %rbx + ret + +# bi_gc_stats: (gc-stats) -> (cons collections live-bytes) +bi_gc_stats: + movq gc_collections(%rip), %rdi + call make_int + movq %rax, %rbx + movq gc_live_bytes(%rip), %rdi + call make_int + movq %rax, %rsi + movq %rbx, %rdi + call make_pair + popq %r12 + popq %rbp + popq %rbx + ret +.endif die_oom: movq $SYS_WRITE, %rax @@ -2619,6 +3291,12 @@ eval_list: je bi_hash_set_size cmpq $BI_HS_LIST, %rax je bi_hash_set_to_list +.ifdef GC_NAIVE + cmpq $BI_GC_COLLECT, %rax + je bi_gc_collect_user + cmpq $BI_GC_STATS, %rax + je bi_gc_stats +.endif movq $VAL_VOID, %rax popq %r12 diff --git a/examples/bench-gc-memory.lsp b/examples/bench-gc-memory.lsp new file mode 100644 index 0000000..3de2a49 --- /dev/null +++ b/examples/bench-gc-memory.lsp @@ -0,0 +1,33 @@ +;;; 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) diff --git a/tests/bench-gc-memory.sh b/tests/bench-gc-memory.sh new file mode 100755 index 0000000..97dac03 --- /dev/null +++ b/tests/bench-gc-memory.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# bench-gc-memory.sh — compare asm no-GC vs asm naive GC under +# sustained allocation load. Each build runs the same .lsp workload +# while a sampler records RSS from /proc//status. Reports +# wall time, peak RSS, final RSS. +set -e +cd "$(dirname "$0")/.." + +ulimit -v 524288 +trap 'pkill -9 -u "$USER" -f "asm/uncommonlisp" 2>/dev/null || true' EXIT + +make -s -C asm all + +WORKLOAD=examples/bench-gc-memory.lsp + +run_with_sampling() { + local bin="$1" + local label="$2" + local pid peak=0 final=0 rss + echo "== $label ==" + "$bin" < "$WORKLOAD" > /tmp/gc-bench.out & + pid=$! + while kill -0 "$pid" 2>/dev/null; do + rss=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0) + if [ -n "$rss" ] && [ "$rss" -gt "$peak" ]; then + peak=$rss + fi + final=$rss + sleep 0.05 + done + wait "$pid" + cat /tmp/gc-bench.out + echo "peak_rss_kb=$peak" + echo "final_rss_kb=$final" + echo "" +} + +run_with_sampling ./asm/uncommonlisp "asm (no GC, 64 MB chunks)" +run_with_sampling ./asm/uncommonlisp-gc "asm (naive GC, 1 MB chunks)" + +rm -f /tmp/gc-bench.out + +if pgrep -u "$USER" -f 'asm/uncommonlisp' > /dev/null; then + echo "STRAGGLER detected" >&2 + exit 1 +fi