ReviewOS

zig-utils/zig-js

Perf: service VM safepoints without generic object-churn fallback iterations

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

Parent: #97 Related: #62, #98, #102

Evidence

A fresh 10-second ReleaseFast sample of clean 24656d74, exact shared object_churn 8x100, shows these collapsed top-of-stack counts across the worker threads:

  • gc.allocObjectBatch: 5,759
  • Heap.headerForPayload: 5,574
  • Heap.create: 5,413
  • __ulock_wait2: 2,712
  • Heap.sweepPhaseLocked: 2,170
  • GcCellBacking.allocFn: 1,574
  • Heap.collectYoung: 1,299
  • GcCellBacking.freeFn: 65

The retained object-allocation quick loop accounts 61 VM steps per iteration and is bounded to the next 1,024-step safepoint. A 16-iteration batch consumes 976 logical steps. When fewer than 61 steps remain, the quick path returns null and the interpreter executes one complete generic iteration. That generic iteration uses unbatched Heap.create, tolerant barrier classification through headerForPayload, and parallel metadata locking. All eight workers repeatedly converge on that fallback around collection boundaries.

Direction

Allow an already-guarded, non-calling quick-loop iteration to cross at most one internal VM safepoint, then immediately service the crossed checkpoint from fully materialized frame/stack state before any next JS instruction. Preserve exact maximum-step behavior and do not allow arbitrary checkpoint skipping. Prefer a general quick-loop checkpoint handoff if it is safe for the other guarded loop plans.

Acceptance criteria

  • Never execute the generic loop body solely because less than one specialized iteration remains before a safepoint.
  • Service every crossed checkpoint immediately after the atomic specialized iteration, with live accumulator/IP/frame roots materialized.
  • Preserve stop/termination, GIL-yield, GC safepoint, maximum-step, exception-step, and observable re-entry semantics.
  • Add boundary tests immediately before/at/after a 1,024-step checkpoint and near max_steps.
  • Exact-parent direct and shared 1/2/4/8 object_churn A/B keeps exact checksums and materially reduces Heap.create/headerForPayload samples.
  • Pass focused VM/GC/no-GIL tests, full units once, and relevant TSan/threadfuzz gates.
  • Refresh the full report, TSV, benchmark guide, and README after a retained change.
9 days ago

First retained slice shipped directly to main:

  • 2dac7aca — single-mutator fixed-shape allocation may finish one guarded 61-step iteration across a VM checkpoint, then uses the existing observable-step helper to service every crossed checkpoint from materialized accumulator/IP/frame roots.
  • Shared/concurrent-GC mode deliberately keeps the prior checkpoint bound; the prototype that enabled crossing there regressed exact shared 8x100 by 23.74% in the short run and was rejected.
  • ab386010 — refreshed the full report, all 1,540 raw samples, benchmark guide, and README from clean implementation commit 2dac7aca.

Exact-parent object_churn A/B (exact checksums):

  • direct: 177.259 -> 152.061 ms, 16.57% faster
  • warmed independent 8x100: 480.678 -> 399.260 ms, 20.39% faster
  • cold independent 8x100: 454.919 -> 417.271 ms, 9.02% faster
  • short shared 8x100: 533.170 -> 524.945 ms, 1.57%, treated as unchanged because the retained shared path is behaviorally unchanged

Fresh full publication from 2dac7aca:

  • direct object churn: zig-js 148.758 ms vs JSC 114.187 ms
  • warmed independent 8x100: zig-js 505.387 ms vs JSC 177.359 ms
  • cold independent 8x100: zig-js 463.413 ms vs JSC 182.816 ms
  • shared 1/2/4/8: 377.316 / 741.069 / 2,316.414 / 9,789.216 ms
  • geometric-mean zig-js lead: 2.69x direct, 2.71x warmed 8-lane, 2.77x cold 8-lane

Validation:

  • focused VM checkpoint test green
  • focused fixed-shape allocation TSan coverage 4/4 green, including real shared Thread workers
  • full units 763/763 green
  • publication harness 7/7 green; TSV contains exactly 1,540 samples

Leaving this issue open. The serial/independent slice is useful and published, but the acceptance criteria still require a shared-safe design, explicit immediately-before/at/after and near-max_steps boundary coverage, and shared profile evidence showing that the generic Heap.create/headerForPayload convoy is materially reduced.

9 days ago

Second retained slice shipped to main:

  • 5f891bc8 — shared/concurrent-GC fixed-shape allocation now finishes the guarded crossing iteration, services the crossed checkpoint from materialized VM state, and keeps the 17th iteration inside the original bulk allocation instead of opening a contended one-object batch. Error paths advance through the same observable-step helper so a logically earlier termination/GIL/GC checkpoint wins before the exception.
  • 409d6a30 — refreshed the full generated report, all 1,540 raw samples, benchmark guide, and README from clean implementation commit 5f891bc8.

Matched 10-second profiles (exact baseline vs candidate):

  • generic Heap.create: 6,135 -> 1,257 samples (-79.5%)
  • headerForPayload: 7,442 -> 1,899 samples (-74.5%)
  • checksums remained exact

Development A/B retained direct neutrality and improved shared 1/2/4/8 medians. The clean full publication shows, versus the previous full publication:

  • warmed independent 8x100: 505.387 -> 419.609 ms (+20.4% throughput)
  • cold independent 8x100: 463.413 -> 417.247 ms (+11.1% throughput)
  • shared 1x100: 377.316 -> 351.344 ms (+7.4% throughput)
  • shared 2x100: 741.069 -> 717.910 ms (+3.2% throughput)
  • shared 4x100: 2,316.414 -> 2,112.202 ms (+9.7% throughput)
  • shared 8x100: 9,789.216 -> 9,311.238 ms (+5.1% throughput)
  • direct: 148.758 -> 150.652 ms (-1.3% throughput), treated as ordinary cross-run/power variation because the direct allocation path is unchanged

Correctness/validation:

  • explicit first specialized iteration placement immediately before, exactly at, and immediately after a 1,024-step checkpoint
  • exact max_steps + 1 and stop-at-checkpoint coverage
  • quick-loop allocation/restricted/fallback errors now use observable checkpoint advancement
  • focused regular test 2/2 green
  • focused TSan 4/4 green, including real shared Thread workers
  • full units 763/763 green, no failures/skips/leaks
  • publication harness 7/7 green; exactly 1,540 raw samples

The implementation and documentation acceptance criteria are now covered. I am leaving the issue open until the new 409d6a30 CI run completes its TSan/threadfuzz gates; #97 remains open because shared eight-lane object churn still scales only 0.30x and needs the next allocator/GC optimization.

9 days ago

The checkpoint-crossing batch is complete in 5f891bc and documented in 409d6a3. Its focused regular and TSan real-Thread tests passed, the later full engine suite passed 764/764 with no skips/failures/leaks, and #105 built successfully on top of it. The latest full publication is 1391131. Closing this completed slice; remaining shared object-churn work stays tracked by #97 and #105.

Sign in to comment on this issue.