================================================================ blaise_mem allocator — benchmark log ================================================================ Tracks performance of the Pascal memory allocator (runtime/src/main/pascal/blaise_mem.pas) over time. One entry per measurement session. Each entry records: - Date - Git revision and short commit message describing the state - Host environment (CPU, kernel, gcc) when it changes - Two columns: libc malloc baseline vs blaise_mem - Ratio (blaise_mem / malloc) — lower is better; <1.0 means we beat malloc - Notes describing what changed since the last entry Run procedure ------------- 1. cd runtime && make && make install && cd .. 2. Recompile both benchmark programs against the fresh RTL: compiler/target/blaise --source runtime/src/test/pascal/bench_libc_malloc.pas ... compiler/target/blaise --source runtime/src/test/pascal/bench_blaise_mem_custom.pas ... bench_blaise_mem.pas uses the GetMem builtin and post-cutover measures blaise_mem (same as bench_blaise_mem_custom.pas). Use bench_libc_malloc.pas for the libc baseline. 3. Run each binary three times; record the median. 4. Keep the same workload constants in both programs: SMALL_COUNT = 1 000 000 x 32 B MIXED_COUNT = 500 000 x 8/32/128/512/2048 B REALLOC_COUNT = 100 000 x 5 grow steps LARGE_COUNT = 10 000 x 65 536 B RETAIN_COUNT = 100 000 x 64 B then bulk-free Host environment ---------------- CPU : AMD Ryzen 7 5800X 8-Core (x86_64) Kernel : Linux 6.14.0-37-generic Toolchain: gcc 13.3.0, QBE 1.3 Workload columns ---------------- S = Small alloc/free (1M x 32 B) M = Mixed sizes (500k x 8-2048 B) R = Realloc growth (100k x 5 steps) L = Large alloc/free (10k x 64 KB) H = Retain + free-all (100k x 64 B) All values in milliseconds. Ratio = blaise_mem / malloc per workload; "—" when malloc baseline is 0 ms. ================================================================ 2026-06-02 — commit c9dde98 — vendor QBE 1.2 → 1.3 ================================================================ Workload | malloc | blaise_mem | ratio ---------+--------+------------+-------- S | 7 | 7 | 1.00x M | 5 | 4 | 0.80x R | 5 | 8 | 1.60x L | 0 | 0 | — H | 4 | 4 | 1.00x Notes: QBE updated from v1.2 to v1.3. Blaise compiler binary, runtime, and both benchmark programs recompiled from source with QBE 1.3. Median of three runs each. No measurable change versus the previous QBE 1.2 baseline (inlining phase 2 entry, 2026-05-16). This is expected: blaise_mem is a runtime benchmark and QBE 1.3's new passes (GVN, GCM, if-conversion) act on emitted IR. The compiler binary itself was compiled before QBE 1.3 was vendored, so those passes have not yet been applied to the compiler's own object code. Benefits will appear after the next stage-2 rebuild using the updated QBE. Full test suite wall time (QBE 1.2 binary, baseline): ~12.60 s Full test suite wall time (QBE 1.3 stage-2 binary): ~12.39 s Compile step only (QBE 1.2): 7.39 s → (QBE 1.3): 7.13 s (−3.5%) TestRunner execution: 5.21 s vs 5.22 s (noise, I/O-bound). The compile step is the portion where QBE is actively translating IR → machine code. The −3.5% improvement in that step with the QBE 1.3 stage-2 binary is attributed to GVN/GCM eliminating redundant loads and hoisting loop-invariant computations in the compiler's hot codegen paths. Fixpoint clean (stage-2 IR == stage-3 IR, 166479 lines). ================================================================ 2026-05-16 — commit d7e9caa — full cutover to blaise_mem ================================================================ Workload | malloc | blaise_mem | ratio ---------+--------+------------+-------- S | 8 | 7 | 0.88x M | 5 | 4 | 0.80x R | 5 | 8 | 1.60x L | 0 | 0 | — H | 5 | 4 | 0.80x Notes: The compiler RTL, codegen builtins, and all C runtime files now route allocations through _BlaiseGetMem / _BlaiseFreeMem / _BlaiseReallocMem. libc malloc is no longer used for Blaise strings, class instances, dynamic arrays, or explicit GetMem. Median of three runs each. blaise_mem now beats libc on three workloads (S, M, H). The R workload (1.6x) remains the only gap. glibc memcpy uses SSE moves for the 16->32->64->128->256 byte realloc chain; closing that needs in-place arena-tail growth with a memcpy fast path in the Blaise allocator. Real-world compile workload (Blaise test suite, 1934 tests): Pre-cutover (commit b815793): ~87-89 s Post-cutover (commit d7e9caa): ~80 s (~10% faster) The compile-time win exceeds the microbench gap on R because real compilation is dominated by short-lived small allocations (string ARC, parser AST nodes), where blaise_mem's freelist pop/push beats glibc tcache's bookkeeping. A third bench source was added: bench_libc_malloc.pas, which uses explicit external 'malloc'/'free'/'realloc' bindings so the libc baseline can still be measured post-cutover. Fixpoint clean (stage-3 == stage-4, achieved at stage-3). Diagnostic _StringReleaseCheck (commit a2c55be) catches header corruption in _StringRelease before the wild segfault. Not triggered during the cutover test run; left in for future heap-mismatch debugging. ================================================================ 2026-05-16 — commit 13bfbd3 — inlining phase 2 ================================================================ Workload | malloc | blaise_mem | ratio ---------+--------+------------+-------- S | 7 | 8 | 1.1x M | 5 | 4 | 0.8x R | 5 | 8 | 1.6x L | 0 | 0 | — H | 5 | 4-5 | 0.9x Notes: Phase 2 of the inliner: case statements and larger bodies (cap raised from 8 to 24 statements) are now eligible. This unlocks inlining of SizeClassIndex (8 if/then/else chain) and SizeClassBytes (case statement), both used by every call to _BlaiseGetMem, _BlaiseFreeMem, and RoundUpToClass. Median of three runs each. Movement since c16249a (phase 1 inlining): S: 9 -> 8 ms (-11%) M: 5 -> 4 ms (-20%) R: 10-11-> 8 ms (-20-27%) <-- headline target L: 0 -> 0 ms (unchanged) H: 5 -> 4-5 ms (small win) R workload now within 1.6x of malloc (was 2.1x). The remaining gap is in glibc memcpy SSE moves vs Blaise's per-byte fallback when realloc crosses size classes. Fixpoint clean (stage-2 IR == stage-3 IR). ================================================================ 2026-05-16 — commit c16249a + bootstrap binary refreshed ================================================================ Workload | malloc | blaise_mem | ratio ---------+--------+------------+-------- S | 7 | 9 | 1.3x M | 5 | 5 | 1.0x R | 5 | 10-11 | 2.1x L | 0 | 0 | — H | 5 | 5 | 1.0x Notes: Captures the cumulative effect of three changes shipped in this session, with the v0.8.0 bootstrap binary refreshed in-place against the verified stage-3 fixpoint of the new source. Contributing changes since the 2026-05-16 first-row baseline: 9847369 fix(rtl): restore large-alloc LIFO cache in blaise_mem. L workload 33 ms -> 0 ms (matches malloc). b4ec712 feat(codegen): promote tyPointer and tyPChar locals to QBE temps. Foundational; no direct workload change but unlocks downstream wins by removing stack slot loads on every pointer access. 314b537 feat(semantic): mark inline candidates. Analyser only; no codegen change yet. c16249a feat(codegen): inline small leaf functions at call sites. 20 candidates inlined compiler-wide (RoundUpToClass, MapFailed, PageRound, MemCopy, StrData, _StringLength, etc.). R workload 12-13 ms -> 10-11 ms (-15%). S workload 10 -> 9 ms. Compared to the original 2026-05-16 first-row baseline (before the IsLarge fix landed): S: 14 -> 9 ms (-36%) M: 8 -> 5 ms (-38%) R: 13 -> 10 ms (-23%) L: 33 -> 0 ms (now matches malloc) H: 5 -> 5 ms (unchanged; already at malloc parity) Open gaps (next targets): R workload (2.1x malloc) — remaining gap is in the SmallGetMem/memcpy/SmallFreeMem fallback path that fires when realloc crosses size classes. Inlining SizeClassIndex and SizeClassBytes would help further; both currently rejected by the analyser (case stmt, > 8 statements). S workload (1.3x malloc) — freelist push/pop is already O(1); the remaining gap is glibc tcache thread-local caching. Not closing this without a redesign. Bootstrap binary: releases/v0.8.0/blaise replaced in-place with the stage-3 fixpoint binary. Fixpoint now converges at stage-2/3 (was stage-3/4), so future fixpoint runs are faster (~2 min instead of ~4). ================================================================ 2026-05-16 — investigation: in-place arena-tail realloc growth ================================================================ Approach (not committed): In _BlaiseReallocMem, detect when the block being realloc'd sits at the tail of the head arena (block end == arena bump pointer). When the new size class still fits in the arena, extend in place by advancing the offset and updating the header — saves the SmallGetMem + memcpy + SmallFreeMem round trip. Measured hit rate on R workload: 100% (every single realloc takes the in-place path). Measured R workload time: baseline (memcpy path) : ~13 ms in-place path : ~23-26 ms (REGRESSION) Why slower despite saving a memcpy: Blaise's codegen does not register-allocate locals — every local becomes a stack slot via alloc4 / alloc8 and every read/write goes through memory. The added checks (ArenaHead load, A^.Base + A^.Offset arithmetic, two calls to RoundUpToClass) materialise as ~30 extra memory operations per realloc. glibc memcpy for 16-128 byte payloads is one or two SSE moves — faster than the overhead our function-call prologue adds. Conclusion: this microbenchmark is codegen-limited, not algorithm-limited. Closing the gap to malloc on small-realloc workloads needs compiler-side improvements first: - Function inlining for small leaf functions (RoundUpToClass, SizeClassIndex, SmallGetMem fast path). - Register allocation so locals do not round-trip through alloc4/alloc8 stack slots. Both improvements would benefit every workload, not just the allocator. Deferring algorithmic allocator tuning until codegen catches up. Lesson recorded for future allocator work: when borrowing ideas from FPC heap.inc or glibc malloc, benchmark them — the better algorithm is not always the faster one in a poorly-optimised compiler. ================================================================ 2026-05-16 — commit b1cb9d0 + IsLarge() fix (uncommitted) ================================================================ Workload | malloc | blaise_mem | ratio ---------+--------+------------+-------- S | 7 | 10 | 1.4x M | 5 | 9 | 1.8x R | 5 | 20 | 4.0x L | 0 | 0 | — H | 5 | 5 | 1.0x Notes: First entry on the new tracking format. Captures the state immediately after fixing the LIFO-cache bug in LargeGetMem / LargeFreeMem. Bug fixed: 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 slot overlapped with the high half of AllocSize and was always zero. Every large free therefore went through the small-block path and never populated the LIFO cache, forcing a fresh mmap on every large alloc (~32 ms for 10k x 64 KB). Fix: 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() reads the correct value, free routes through LargeFreeMem, and the cache reaches ~100 % hit rate on the L workload. Result: L workload: 32 ms → 0 ms (matches malloc) Cache stats (10 000 calls): 9 999 hits, 1 mmap. Open gaps (next targets): R workload (4.0x malloc) — realloc still falls back to copy on small-block size-class boundaries. Investigate in-place small-block growth when the next size class still fits the same arena slot. M workload (1.8x malloc) — extra header bookkeeping per small alloc. Possible win: drop per-block AllocSize when the size class already encodes it. S workload (1.4x malloc) — freelist push/pop overhead vs glibc tcache. Likely needs per-thread caching long term. ================================================================ 2026-05-16 — commit b1cb9d0 (pre-fix baseline, for reference) ================================================================ Workload | malloc | blaise_mem | ratio ---------+--------+------------+-------- S | 7 | 14 | 2.0x M | 5 | 8 | 1.6x R | 5 | 13 | 2.6x L | 0 | 33 | — (cache broken) H | 5 | 5 | 1.0x Notes: Initial Pascal-allocator measurement after introducing blaise_mem. LIFO cache for large allocations was wired up in code but silently broken (see 2026-05-16 fix above). Kept here so the impact of the fix is visible in the log.