ReviewOS

zig-utils/zig-js

Perf: amortize GC size-class locks across object allocation batches

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

Problem

After #104 removed most generic VM fallback allocation, the dominant 8-lane shared object-churn cost moved into the GC cell backing. A fresh 10-second sample recorded:

  • GcCellBacking.allocFn: 10,150 samples
  • gc.allocObjectBatch: 6,955 samples
  • heap.Heap.sweepPhaseLocked: 1,976 samples
  • heap.Heap.headerForPayload: 1,406 samples
  • generic Heap.create fallback: 1,055 samples

zig-gc createBatch publishes metadata once, but requests each of the 17 same-size slabs separately. GcCellBacking consequently acquires the same bucket lock 17 times.

Parent: #97 Depends on: zig-utils/zig-gc#18 Related: #102, #104

Direction

Implement the zig-gc optional batch-backing hook in GcCellBacking so one allocation batch acquires its size-class lock once, fills freed slots first, then fresh slots, and returns the successful prefix. Keep chunk growth, ownership indexes, reuse counters, teardown behavior, and allocator fallback exact.

Acceptance

  • Focused tests prove one bucket-lock acquisition for an eligible batch.
  • Reuse and fresh allocation can coexist in one batch without duplicate slots or incorrect counters.
  • Short-prefix/OOM behavior preserves sequential recovery ordering.
  • Focused regular and TSan shared-worker coverage passes.
  • Exact A/B covers direct and shared 1/2/4/8 lanes with stable checksums.
  • A retained material change refreshes README.md, docs/benchmarks.md, and the raw full benchmark publication before closure.
  • Full zig build test runs once after the retained implementation is ready.
9 days ago

Implementation pushed in 0679295; dependency support is d35570e in zig-gc #18.

Focused A/B against the exact pre-change runner (100 jobs, exact checksums):

  • direct, 21 samples: neutral (about 150.7 ms baseline vs 151.2 ms candidate)
  • shared 1 lane, 7 samples: 351.6 ms -> 349.2 ms
  • shared 2 lanes, 7 samples: 687.4 ms -> 664.6 ms (-3.3%)
  • shared 4 lanes, 7 samples: 2058.4 ms -> 1330.2 ms (-35.4%)
  • shared 8 lanes, 7 samples: 9271.9 ms -> 7895.2 ms (-14.8%)

All checksums matched exactly at every sample.

Validation completed:

  • focused backing telemetry: passed
  • focused real shared Thread worker test: passed
  • same focused test under TSan: passed
  • full zig build test: 764/764 passed, no skips/failures/leaks

The full comparison publication and README refresh are running next; keeping the issue open through publication and CI.

9 days ago

Full publication is pushed in 1391131 from clean implementation commit 0679295.

Published environment: Apple M3 Pro, AC power at 100% charged, Zig 0.17.0-dev.956+2dca735, JSC 22625.1.20.11.3. The report contains 1,540 checksum-validated raw samples.

Current shared object-churn medians (1/2/4/8 lanes): 350.226 / 676.030 / 1403.741 / 7658.643 ms, scaling 1.00x / 1.04x / 1.00x / 0.37x. Against the previous full publication, throughput improved 0.3% / 6.2% / 50.5% / 21.6%. Published RSD is high (25-44%), so README and docs explicitly use the focused exact-runner A/B as causal evidence and retain the full raw range.

Current matrix headlines: direct zig-js lead 2.67x GM; warmed independent-8 lead 2.75x; cold independent-8 lead 2.75x; shared-8 scaling 4.09x GM. Object churn remains the sole direct JSC loss.

Docs validation: bun run docs:build passed. Keeping this issue open only until the latest combined CI run completes.

9 days ago

Completed by 0679295 and publication 1391131. The consumer and dependency validation is included in the fully green combined main run https://github.com/zig-utils/zig-js/actions/runs/29381215887. Follow-up weak-pass work is complete in #106; the parent scaling target remains #97.

Sign in to comment on this issue.