Records allocator microbenchmarks and compile-step timing after
updating the vendored QBE from v1.2 to v1.3. The compile step
(QBE translating IR to machine code) is 3.5% faster in the stage-2
binary assembled by QBE 1.3, attributed to the new GVN/GCM passes.
Add bench_libc_malloc.pas with explicit external malloc/free/realloc
bindings so the libc baseline can still be measured after the cutover.
The original bench_blaise_mem.pas now measures blaise_mem too (because
the GetMem builtin emits _BlaiseGetMem), so it is no longer suitable
as a baseline.
Log the post-cutover state: blaise_mem now beats libc on small, mixed,
and retain-free-all workloads. Realloc growth remains 1.6x. The
compile-time win on the real test suite (~10%) exceeds the microbench
gap, suggesting real workloads are dominated by short-lived small
allocations where the freelist pop/push beats glibc tcache.
R workload moved 10-11 ms -> 8 ms (-20 to -27%) after enabling case
statements and larger bodies in the inliner. M and H also improved
modestly. S is essentially unchanged. R now 1.6x malloc, down from
2.1x.
Records the cumulative effect of the IsLarge fix, pointer-promotion
codegen, inline-candidate analyser, and leaf-function inlining
landed this session. Bootstrap binary refreshed in-place;
releases/v0.8.0/blaise is now the verified stage-3 fixpoint of
the current source, so fixpoint converges at stage-2/3.
Headline numbers vs the original 2026-05-16 baseline:
Small alloc/free: 14 -> 9 ms (-36%)
Mixed sizes: 8 -> 5 ms (-38%)
Realloc growth: 13 -> 10 ms (-23%)
Large alloc/free: 33 -> 0 ms (matches malloc)
Retain + free-all: 5 -> 5 ms (unchanged)
Records the negative result from today's in-place arena-tail growth
experiment in blaise_mem: 100% hit rate but a 2x regression
(13 ms -> 23 ms) on the R workload. Root cause is Blaise's
codegen lacking inlining and register allocation for locals — the
added checks cost more memory traffic than the saved memcpy of
16-128 byte payloads.
The note documents the lesson so future allocator-perf work can
skip re-doing the experiment: closing the malloc gap on
small-realloc workloads requires compiler-side improvements
(inlining + register allocation) first.
IsLarge() was reading the small-header Flags field at offset Ptr-4,
but TLargeHeader laid its AllocSize: Int64 across Ptr-8..Ptr-1, so
the Flags probe overlapped with the high half of AllocSize and was
always zero. Every large block therefore routed through the small
free path and the LIFO cache was never populated, forcing a fresh
mmap on each large allocation.
Restructured TLargeHeader to:
TotalMapped: Int64 (Ptr-16..Ptr-9)
AllocSize: Integer (Ptr-8..Ptr-5)
Flags: Integer (Ptr-4..Ptr-1)
LargeGetMem now writes Flags := FLAG_LARGE, IsLarge() returns the
correct value, and the cache reaches ~100% hit rate on the large
alloc/free workload (32 ms -> 0 ms for 10k x 64KB).
Also adds the two benchmark programs (bench_blaise_mem.pas for the
malloc baseline and bench_blaise_mem_custom.pas for blaise_mem) and
reformats docs/benchmark.txt as a dated log so future runs can be
tracked over time.