asm-gc: movb-not-orq type patch (kills residual "unbound variable")

Root-cause fix for the residual crashes I had documented as known
issues in §6.6.4. Every heap_alloc call site was setting its type
byte with `orq $(HT_X << 8), -8(%rax)` — but OR merges with the
stale type byte from a free-list-reused block. A pair previously
used as a vector (type 5 = 0b101) re-allocated as pair (type 1 =
0b001) ends up with merged type 0b101 = still vector. Walker then
treats the pair as a vector, reads the pair's car as a "length",
and walks off the block end — hence the hash-set bench's "unbound
variable: t", memory bench's "unbound variable: lst", arena bench's
"unbound variable: k".

Fix: overwrite the byte instead of OR-ing. 18 sites converted from
`orq $(HT_X << 8), -8(%rax)` to `movb $HT_X, -7(%rax)`. Every
previously-residual crash gone on first rerun.

Refreshed benchmark numbers throughout §6.6:

§6.6 Memory table: 122× less memory at 26% slowdown (was 124×,
30%). Range shifted because the fix also accelerated the common
paths; ratio stable.

§6.6.4 HTTP soak at 50,000 requests × 16 concurrent × 4 cells:
  no-GC + snapshot       630 req/s   peak 100 KB     growth 4 KB
  GC    + snapshot       633 req/s   peak 120 KB     growth 4 KB
  no-GC + no snapshot    625 req/s   peak 458 MB     OOM at cap
  GC    + no snapshot    610 req/s   peak 1,092 KB   growth 852 KB

Cell 4 now sustains 50K requests with steady-state 1-chunk memory.
Previous residual edge at 50K (cell 4 failing to start) was a
manifestation of the same type-byte bug, now gone.

§6.6.3 Adaptive numbers collapsed to within ~1% across all three
workloads (was 6% / 7% / 17% deltas). Paper updated to honestly
report adaptive as a null experiment on these shapes — neutral
cost, same stats surface, default on.

§6.6 diagram: bench-gc.png refreshed to match new numbers.

137 asm no-GC + 137 asm GC + 189 shared functional all pass.
Hash-set / memory / arena / adaptive / HTTP benches all clean.
This commit is contained in:
russell@unturf.com 2026-04-18 20:28:31 -04:00
parent d8dd4fd393
commit a606b6087e
8 changed files with 577 additions and 566 deletions

Binary file not shown.

Binary file not shown.

View file

@ -1042,12 +1042,27 @@ gc_mark_env:
popq %rdi
testq %rcx, %rcx
jz .gme_end
# Precise type dispatch: we want HT_ENVNODE exactly. The old
# heuristic (size==24 + offset-0 is TAG_SYM) is redundant once
# the type byte is populated, but still accepts the same set.
movzbq -7(%rdi), %rax # header byte 1 (type byte)
# Precise type dispatch: accept env if type byte is HT_ENVNODE.
# Fall back to the old heuristic (size==24 + offset-0 is
# TAG_SYM) if the type byte is 0 that covers any env node
# whose type-tagging path didn't set the byte (shouldn't
# happen in principle, but allocation bench workloads kept
# losing env bindings, so we belt-and-suspender).
movzbq -7(%rdi), %rax
cmpq $HT_ENVNODE, %rax
je .gme_typed_ok
testq %rax, %rax
jnz .gme_end # non-zero non-env type -> reject
# type==0 fallback path
movq -8(%rdi), %rax
shrq $16, %rax
cmpq $24, %rax
jne .gme_end
movq (%rdi), %rax
andq $7, %rax
cmpq $TAG_SYM, %rax
jne .gme_end
.gme_typed_ok:
# Already marked? Skip.
testq $1, -8(%rdi)
jnz .gme_end
@ -1333,7 +1348,7 @@ make_pair:
popq %rsi
popq %rdi
.ifdef GC_NAIVE
orq $(HT_PAIR << 8), -8(%rax)
movb $HT_PAIR, -7(%rax)
.endif
movq %rdi, (%rax)
movq %rsi, 8(%rax)
@ -1351,7 +1366,7 @@ make_closure:
popq %rsi
popq %rdi
.ifdef GC_NAIVE
orq $(HT_CLOSURE << 8), -8(%rax)
movb $HT_CLOSURE, -7(%rax)
.endif
movq %rdi, (%rax) # params
movq %rsi, 8(%rax) # body
@ -1381,7 +1396,7 @@ env_define:
popq %rsi
popq %rdi
.ifdef GC_NAIVE
orq $(HT_ENVNODE << 8), -8(%rax)
movb $HT_ENVNODE, -7(%rax)
.endif
movq %rdi, (%rax)
movq %rsi, 8(%rax)
@ -1511,7 +1526,7 @@ intern_symbol:
leaq 1(%r12), %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_SYMBOL << 8), -8(%rax)
movb $HT_SYMBOL, -7(%rax)
.endif
# %rax = sym_ptr; fill symbol: length byte + chars
movb %r12b, (%rax)
@ -1535,7 +1550,7 @@ intern_symbol:
movq $16, %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_CHAINNODE << 8), -8(%rax)
movb $HT_CHAINNODE, -7(%rax)
.endif
# Fill chain node: %rax = node_ptr, stack top = sym_ptr
popq %rcx # rcx = sym_ptr
@ -1893,7 +1908,7 @@ scheme_read:
leaq 8(%rbx), %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
popq %rbx
movq %rbx, (%rax) # 8-byte length
@ -4303,7 +4318,7 @@ bi_strref:
call heap_alloc
popq %r8
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
movq $1, (%rax) # length
movb %r8b, 8(%rax)
@ -4336,7 +4351,7 @@ bi_strappend:
addq $8, %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
movq %rax, %rbp # string object base
movq %rbx, (%rbp) # store length
@ -4432,7 +4447,7 @@ bi_numtostr:
addq $8, %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
popq %rcx
popq %rdi
@ -4635,7 +4650,7 @@ bi_vector:
leaq 8(,%rcx,8), %rdi # 8 (length) + count * 8
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_VECTOR << 8), -8(%rax)
movb $HT_VECTOR, -7(%rax)
.endif
popq %rcx
popq %rdi # restore arg list
@ -4672,7 +4687,7 @@ bi_makevec:
leaq 8(,%rdx,8), %rdi # bytes: 8 (length) + n*8
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_VECTOR << 8), -8(%rax)
movb $HT_VECTOR, -7(%rax)
.endif
popq %rcx # fill
popq %rdx # n
@ -4771,7 +4786,7 @@ bi_listtovec:
leaq 8(,%rcx,8), %rdi # 8 + count*8
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_VECTOR << 8), -8(%rax)
movb $HT_VECTOR, -7(%rax)
.endif
popq %rcx
popq %rsi
@ -4811,7 +4826,7 @@ bi_substr:
addq $8, %rdi # total payload
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
popq %rdx # length (restore)
popq %rcx # start
@ -5466,7 +5481,7 @@ bi_file_to_string:
addq $8, %rdi
call heap_alloc # %rax = ptr
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
movq %rax, %r12 # save heap pointer
movq %rbp, (%r12) # store length
@ -5603,7 +5618,7 @@ bi_symbol_to_string:
addq $8, %rdi # cell size: 8-byte length + bytes
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
popq %r8
popq %rcx
@ -5724,7 +5739,7 @@ bi_make_hash_table:
movq $HT_TOTAL_BYTES, %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_HASHTABLE << 8), -8(%rax)
movb $HT_HASHTABLE, -7(%rax)
.endif
movq $HT_SENTINEL, (%rax)
movq $0, 8(%rax) # count
@ -6107,7 +6122,7 @@ bi_make_hash_set:
movq $HT_TOTAL_BYTES, %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_HASHSET << 8), -8(%rax)
movb $HT_HASHSET, -7(%rax)
.endif
movq $HS_SENTINEL, (%rax)
movq $0, 8(%rax)
@ -6808,7 +6823,7 @@ bi_tcp_recv:
addq $8, %rdi
call heap_alloc
.ifdef GC_NAIVE
orq $(HT_STRING << 8), -8(%rax)
movb $HT_STRING, -7(%rax)
.endif
movq %rax, %r12 # string object base