Skip to content

perf(gc): per-object layout metadata is address-keyed, so every copying minor rehashes it — 304 MB allocated / 14.9 MB live per 400-char reply #9792

Description

@proggeramlug

Summary

Per-object layout metadata lives in address-keyed side maps, so a moving
collector has to rehash the whole per-object layout world on every copying
minor. Measured on the compiled claude-code TUI with PERRY_ALLOC_CENSUS
(#9771), one 400-character streamed reply:

gc/layout.rs + gc/layout_tables.rs allocate 304 MB and hold 14.9 MB live
per turn.
For scale, the whole process allocates 22.5–23.5 GB per such turn
with a 1.70 GB peak live, and mimalloc's committed set — which is the process
footprint on macOS, every dirty page being Memory Tag 240 — tracks that peak.

Per-callee

site allocated live
layout::layout_transfer (from CopyingNurseryCollector::mark_addr) ~50 MB 0
layout_tables::prune_dead_per_object_layout_owners ~45 MB 0
layout_tables::slot_masks_insert (from layout_rebuild_from_slots, js_closure_alloc_init) ~15 MB 0
HashMap<usize, Vec<(usize, usize)>>::insert — the per-object layout table itself 8.7 MB 8.7 MB
remainder (inlined layout machinery under mark_addr, trace_heap_rewrite_slots) ~185 MB ~6 MB
total 304 MB 14.9 MB

85 % of the process's whole allocation volume is one size class — 19,074 MB in
1.56 M allocations of 8–16 KB — which is what hash-table rehashing and Vec
growth look like from the allocator's side.

Mechanism

layout::layout_transfer(old_user, new_user) runs for every evacuated
object on every copying minor
(CopyingNurseryCollector::mark_addr). It
moves five separate address-keyed records:

Each move is a hash remove plus a hash insert keyed by the object's ADDRESS.
Since the address is exactly what a copying collector changes, the cost is
structural: every surviving object pays several hash operations per minor, and
the tables are regrown as they are rekeyed. prune_dead_per_object_layout_owners
then walks and rebuilds the address filter on top of that.

This is the same defect as A2 in the campaign's ARCHITECTURE.md — metadata
in parallel maps sized and keyed by something the collector moves — in a table
family nobody has looked at yet.

Structural answer

The bits belong in the object header / shape rather than in address-keyed side
maps. _reserved already carries half of them (GC_LAYOUT_STATE_MASK,
GC_LAYOUT_ALL_POINTERS, GC_OBJ_TYPED_LAYOUT_INTACT), and layout_transfer
copies those directly with no hashing at all — that half is already free, and
it is the existence proof that the rest can be. A record carried in the header
moves with the object by construction, and a shape-keyed record needs no move.

What would falsify it — stated before the work

Methodology warning for other lanes

This finding is also a lesson about the instrument. At a 1 MiB sampling
interval the layout bucket read as ~70 MB and an "arena bookkeeping" bucket read
as ~85 MB. At 256 KB the truth was 304 MB and 16 MB respectively —
the arena figure had been inflated by my own bucketing sweeping
arena_alloc_gc_old (which allocates objects) into a bookkeeping bucket, and
the layout figure had been deflated by sampling too coarsely for a long tail of
many small sites. A stated falsifier caught it: the arena hypothesis was
rejected on its own criterion (< 20 MB) and the redirect fell out of the same
run. Sample finely enough that the tail resolves, and check what your buckets
actually match before quoting a total.

Not started

Deliberately not attempted yet: this is more invasive than the evacuation-set
change deferred on #9779, and gc-stress matrix is red on main with #9782 open
(full mark-sweep arms failing), so a green run would prove nothing.

Instrument: PERRY_ALLOC_CENSUS=<path> (#9771), PERRY_ALLOC_CENSUS_INTERVAL=262144.

https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions