ReviewOS

zig-utils/zig-gc

Perf: enumerate owned cells in slab order instead of an intrusive heap list

#33
Closed chris opened this 9 days ago ·
9 days ago

Downstream: zig-utils/zig-js#97 Builds on the negative result in #28; related: #26, #27, #30, #31, #32.

Evidence

The remaining shared object-churn gap is not fixed by lock policy, a pending-stack gate, per-mutator intrusive chains, atomic publication guards, or batched unpublication. #28 removed the heap-wide publication word but regressed 4/8 lanes by 135%/171%; its collector then consumed per-mutator chains whose traversal order no longer matched the backing slabs. Current samples continue to place allocObjectBatch, sweepPhaseLocked, and reusable-slot lookup in the same serialized path.

zig-js already owns every managed cell in persistent size-class chunks with exact published/free bitmaps. Those chunks can provide a stable, address-local enumeration at a collector quiescence boundary, making the intrusive heap-wide all list unnecessary for opted-in embedders and allowing a later publication shard design without destroying sweep locality.

Design direction

Add a generic opt-in owned-cell enumeration contract and a collector mode that treats it as the authoritative live-cell walk:

  • the binding produces a stable snapshot/iterator of published allocation bases only at an explicitly documented heap-quiescent boundary;
  • raw slots reserved by a mutator but not yet header-published must never enter the snapshot;
  • whitening, ephemeron/weak passes, address-index construction, minor/full sweep, and teardown enumerate each live header exactly once in backing-address order;
  • the generic intrusive all list remains the fallback for bindings without the opt-in;
  • allocation/sweep races, header initialization visibility, payload membership, finalization order, and OOM behavior remain sound;
  • once collector locality is proven, retry #28-style private publication without a heap-wide per-batch word.

Acceptance criteria

  • Generic tests cover mixed sizes, holes/reuse, raw unpublished reservations, minor old/young cells, weak work, teardown, and exact once-only enumeration.
  • Parallel tests prove snapshot stability against a publisher stopped before and after header publication; normal and suppression-free TSan gates pass.
  • The opted-in collector does not require the intrusive all prepend on its allocation hot path.
  • Retry private publication with no heap-wide CAS/lock per batch and exact live/byte/nursery accounting.
  • Full zig-gc and focused zig-js semantic/GC gates pass.
  • Exact-parent zig-js object-churn A/B does not regress 1/2 lanes and materially improves both 4 and 8 lanes; otherwise revert the downstream activation and retain only independently useful generic infrastructure.
9 days ago

The combined zig-gc#25 / zig-js#97 tranche work landed before this larger collector rewrite. Thresholded O(1) splicing plus a contended-only 272-cell reserve improves exact 2/4/8-lane object churn by 1.50x / 1.93x / 1.24x and raises 8-lane scaling from 0.52x to 0.65x, while leaving the short-batch path intact. Evidence: https://github.com/zig-utils/zig-js/blob/main/docs/.data/object-churn-amortized-publication-ab-2026-07-16.md

This issue remains open: allocation still uses the intrusive all list, eight-lane scaling remains below 1.0x, and none of the owned-slab authoritative-enumeration acceptance criteria are satisfied by #25. The new profile baseline should be used before attempting this broader collector structure change.

9 days ago

Landed the generic owned-cell enumeration foundation in 30ac716 and activated it in zig-js locally for the exact-parent A/B. Opted-in heaps no longer link allocations into Heap.all; a uniform iterator now drives whitening, conservative index fallback, minor/full scans, ephemeron/after-weak passes, sweep, and teardown, while bindings without the hook retain the original intrusive path. The zig-js backing performs a six-way merge of sorted size-class chunk indexes and filters exact published bitmaps, so private reservations and reclaimed holes are excluded and output is global address order.

Verified so far:

  • zig-gc normal: 42/42, zero leaks
  • zig-gc TSan: 42/42, suppression-free
  • zig-js backing normal + TSan: 25/25 each, zero leaks
  • zig-js nursery/weak/ephemeron: 7/7, zero leaks
  • zig-js no-GIL parallel_gc group: 15/15, zero leaks
  • exact parallel-build -> quiescent collection under TSan: 3/3, zero leaks

I checked only criteria fully evidenced by those gates. The explicit stopped-publisher concurrency test, lock-free aggregate publication follow-up, and exact 1/2/4/8 object-churn A/B remain open.

9 days ago

Exact-parent 1/2/4/8 shared object_churn A/B completed (7 alternating fresh-process pairs per lane, ReleaseFast, every checksum matched). Parent was zig-js 062ef265 + zig-gc c67e344; candidate changed only the local binding activation to zig-gc 30ac716.

Medians and candidate speedups:

  • 1 lane: 137.718 -> 134.511 ms, 1.02x
  • 2 lanes: 186.382 -> 187.012 ms, 1.00x
  • 4 lanes: 425.992 -> 327.088 ms, 1.30x
  • 8 lanes: 2,122.534 -> 2,138.732 ms, 0.99x

The eight-lane gate failed and scaling moved 0.519x -> 0.503x, so I removed the zig-js production binding activation and retained only the generic zig-gc iterator plus the independently useful zig-js backing iterator/tests. This exact design will not be retried. The next experiment must also remove/shard aggregate heap publication rather than changing traversal alone. Full raw/report evidence is being committed under docs/.data/object-churn-owned-enumeration-ab-2026-07-18.{md,tsv}.

9 days ago

Final #33 slice landed in 79b26c1.

The generic heap now combines address-ordered owned enumeration with 64 stable cache-line publication shards. A large owned batch performs no heap alloc_lock, CAS, or global write: it reads the collector gate, updates its thread-owned count and one shard-local byte counter, then publishes the binding bitmap. Collectors close the gate, drain publishers, fold exact live/byte/nursery deltas once, and retain the serialized born-grey fallback throughout marking.

New transition tests stop publishers (1) after raw reservation and before headers, and (2) after complete headers and immediately before bitmap publication. The parallel marker cannot pass either transition incorrectly. Full dependency normal and suppression-free TSan suites pass (44 tests each), including eight-thread exact accounting/minor-sweep coverage with zero allocation-lock acquisitions across 64 batches.

The second exact-parent zig-js A/B still rejects downstream activation: 1/2/4 improve 4.8% / 1.5% / 8.1%, but 8 lanes regresses 6.0% (2/7 pair wins) and scaling falls 0.637x -> 0.574x. The zig-js binding hook was removed; generic infrastructure remains. Evidence is being committed as object-churn-sharded-enumeration-ab-2026-07-18.{md,tsv}. All issue criteria are resolved, including the specified reject-and-revert outcome.

Sign in to comment on this issue.