blaise/docs/benchmark.txt
Graeme Geldenhuys f387bbf2b7 docs(benchmark): log post-inlining state + bootstrap refresh
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)
2026-05-16 14:45:37 +01:00

220 lines
9.1 KiB
Plaintext

================================================================
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_blaise_mem.pas ...
compiler/target/blaise --source runtime/src/test/pascal/bench_blaise_mem_custom.pas ...
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.2
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-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.