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)
220 lines
9.1 KiB
Plaintext
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.
|