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.
303 lines
12 KiB
Plaintext
303 lines
12 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_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.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 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.
|