Shrink the managed-block header on 32-bit targets
- Type: feature (memory) — Track A
- Design:
devdocs/dev/managed-block-header.md. - Not urgent. But its constraint is — see "the part that is time-critical".
The waste
PWord is a pointer to a machine word — 8 bytes on 64-bit, 4 on 32-bit —
while the header's field offsets are fixed 8-byte strides on every target:
[meta:8][refcount:8][length:8][data...] handle = block + 24
So on ILP32 each slot carries a 4-byte value in its low half and 4 bytes of padding: 12 of the 24 header bytes are dead, on every string, dynamic array and object. Packing to 4-byte slots gives a 12-byte header (16 with alignment), roughly halving per-block overhead.
That matters most exactly where memory is scarcest. Under PXX_ESP the heap is a
single 64 KiB static arena (vs a 256 MiB mmap chunk on a host), and xtensa
and riscv32 are both ILP32 — so this is an ESP-capacity feature more than a
desktop one. Frozen string[N]/shortstring already sidestep the header entirely
and remain the primary ESP lever.
The part that is time-critical
Packing makes the meta word 32 bits wide on ILP32. Whatever phase 2
(feature-nilpy-text-string-kind) puts in bits 32–63 would have nowhere to live
there — so either the layouts diverge per target (bad: the field means different
things on different machines) or the upper half must be permanently unused.
So the constraint is already recorded in the design doc and phase 2 must obey
it: every meaningful field lives in the low 32 bits. That is
BlockKind(8) | Flags(8) | KindData0(8) | KindData1(8), with 32–63 reserved.
It also forces KindData0 to hold a small encoding enum rather than a raw
codepage (CP_UTF8 = 65001 does not fit in 8 bits) — which is the better field
anyway.
If this ticket is never done, nothing is lost. If phase 2 spends the upper 32 bits, this ticket becomes impossible. That asymmetry is the whole reason it is filed now at a low priority rather than later.
Sketch
Introduce the stride as a constant (PXX_HDR_SLOT = SizeOf(NativeInt)) and
derive PXX_HDR_SIZE/_META/_RC/_LEN from it, rather than the current fixed
8s. The Pascal side in builtinheap.pas already routes everything through those
names since phase 1, so most of it follows. The work is the x86-64 emitter
(which hardcodes [rax-16], [rsi-32], sub rax, 24 and the inline allocation
sequences) — but note x86-64 is LP64 and would not change; it is the 32-bit
backends that would need their own offsets, and today they hardcode almost
nothing because they delegate managed release to the Pascal runtime. Check that
claim before costing the work: it is what made phase 1 much smaller than
expected.
Gate
Track A, and the same shape as phase 1 — but not via FPC. Seed the self-host
from pinned (which carries its own frozen RTL); make compiler/pascal26 seeds
from a binary with no versioned RTL and will silently produce a core-dumping
compiler ([[bug-a-self-host-seed-has-no-versioned-rtl]]). Then
testmgr --tier full with the 32-bit targets specifically,
-dPXX_HEAP_DEBUG for free-base errors, then stabilize + pin. An ESP/xtensa
build under --platform=esp --esp-profile=bare is the point of the exercise, so
measure the arena headroom before and after.