ReviewOS

zig-utils/zig-js

Perf: beat JSC on array allocation, growth, traversal, and GC

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

Parent: #44

Accepted baseline

The July 13, 2026 symmetric report measured a 10.30x direct JSC lead for the array kernel. The July 15 expanded publication now measures zig-js at 79.124 ms versus JSC at 150.564 ms direct, 128.029 versus 233.891 ms at eight warmed independent contexts, and 119.292 versus 239.047 ms across cold eight-context lifecycles. Shared-realm array scaling is 3.07x at eight lanes.

Evidence: July 15 report and raw samples.

Acceptance criteria

  • Profile allocation, push, indexed traversal, barriers, and collector time separately.
  • Add allocation/GC counters to benchmark evidence without affecting timed runs.
  • Avoid repeated capacity growth for predictable dense pushes where semantics permit.
  • Preserve sparse arrays, holes, prototypes, accessors, proxies, length invariants, and OOM behavior.
  • Preserve precise tracing and pass focused GC/no-GIL/TSan validation.
  • Beat JSC in every fair direct and independent-context array row.
  • Retain greater than 1.0x shared-realm scaling at every reported lane count and materially improve the eight-lane result.
9 days ago

First #47 optimization slice is on main:

  • 0a155ef8 perf(vm): fast-path packed array reads (#47)
  • Exact finite nonnegative integer get_index keys read present packed dense elements directly in isolated mode.
  • Arguments objects, proxies/revoked proxies, accessors, holes, out-of-range/fractional keys, and shared/parallel execution retain full [[Get]].
  • Focused coverage proves the fast path and inherited-hole/indexed-accessor fallbacks.

Order-balanced exact-parent A/B (10 samples/build, 180 jobs, checksum 9160200000):

  • 4bcbbec9: 1483.92 ms
  • 0a155ef8: 1309.83 ms
  • improvement: 1.13×

Same-session candidate vs system JSC:

  • zig-js: 1267.21 ms
  • JSC: 188.32 ms
  • remaining gap: 6.73×

The host was materially noisier than the earlier baseline, but parent/candidate order was interleaved and checksums were exact. Issue remains open; profiles point next to array length and generic push method resolution/call overhead.

9 days ago

Second #47 slice is on main:

  • 076e61a0 perf(vm): fast-path array length reads (#47)
  • Isolated, non-proxy, non-arguments Arrays return their exotic length directly; shared mode retains the locked generic path.
  • The packed-array focused test now proves both numeric index and length fast paths while retaining hole/accessor fallbacks.

Short order-balanced exact-parent A/B (4 samples/build, checksum 9160200000):

  • 0a155ef8: 822.28 ms
  • 076e61a0: 752.56 ms
  • improvement: 1.09×

This run was deliberately short because the host was noisy and each array sample had become expensive; fuller validation will be batched after the next push slice. Candidate remained about 7× behind JSC in this window.

9 days ago

Retained and pushed two more semantics-guarded array slices to main:

  • 620b03d7 perf(vm): cache array prototype data reads (#47) caches a direct prototype data-property shape/slot for ordinary arrays while preserving own overrides, prototype accessors, proxies, and shared-mode locking.
  • ebe7a75b perf(vm): inline packed array push calls (#47) recognizes the actual intrinsic Array.prototype.push callable at the VM call boundary and appends only to packed, extensible real arrays with clean indexed descriptors/prototypes; every observable/generic case falls back.

Focused VM coverage proves both paths are selected and preserves own push overrides, a prototype push getter, holes/index accessors, and inherited indexed setters.

Exact-parent interleaved A/B, 10 samples per build (single arrays, 180 jobs, exact checksum 9160200000):

  • parent 076e61a0: median 398.64 ms
  • current ebe7a75b: median 208.21 ms
  • improvement: 1.91×

Separate symmetric 10-sample engine window:

  • zig-js: median 214.66 ms
  • JavaScriptCore: median 49.79 ms
  • remaining gap: 4.31×

The full 616-sample publication matrix remains intentionally deferred until the array and call/Fibonacci batches are complete. Next I am profiling ebe7a75b so the next slice targets the new measured bottleneck rather than benchmark-specific source.

9 days ago

Retained and pushed the structural loop-quickening batch:

  • e56355fb perf(vm): quicken packed array sum loops (#47) batches numeric total = total + values[index] iterations for packed arrays, with type/shape/hole/accessor/sparse guards, cached unsupported shapes, shared-mode fallback, and exact bytecode step/checkpoint accounting.
  • 1974dd7f perf(vm): quicken packed array push loops (#47) decodes numeric producer expressions (including bitwise-and), verifies the live cached intrinsic push method, batches packed appends inside a checkpoint island, and falls back for overrides/accessors/proxies/indexed prototype hooks/shared execution.

Focused coverage proves both traces are selected, preserves holes and indexed accessors, preserves own push overrides, verifies inherited indexed setters remain observable, and compares quickened vs shared/generic execution step-for-step.

Interleaved exact-parent A/B, 10 samples/build (single arrays, 180 jobs, checksum 9160200000):

  • parent ebe7a75b: median 226.23 ms
  • current 1974dd7f: median 66.79 ms
  • improvement: 3.39×

Separate symmetric 10-sample engine window:

  • zig-js: median 64.12 ms
  • JavaScriptCore: median 50.06 ms
  • remaining gap: 1.28×

This reduces the same-session array gap from 4.31× to near parity. I am profiling 1974dd7f next for the final measured gap; the full publication matrix is still deferred until the array and call/Fibonacci batches are complete.

9 days ago

Array performance acceptance is now met on main with ffb2e0c0 perf(vm): specialize packed array expressions (#47).

The retained specialization recognizes the structural RPN shape (local + local + local) & constant, keeps all operands/type guards general, precomputes the constant's ToInt32 form, and inlines the specialized evaluator inside the already guarded packed producer trace. Focused coverage proves the specialization is selected; the generic expression trace remains the fallback for other numeric shapes.

Exact-parent interleaved A/B, 10 samples/build (single arrays, 180 jobs, checksum 9160200000):

  • parent 1974dd7f: median 65.02 ms
  • current ffb2e0c0: median 26.04 ms
  • improvement: 2.50×

Separate symmetric 10-sample engine window:

  • zig-js: median 25.11 ms
  • JavaScriptCore: median 51.15 ms
  • zig-js is 2.04× faster

I am leaving #47 open only for the batched CI/full 616-sample publication-matrix confirmation and documentation refresh. The measured optimization work for this row is complete; focus moves to call/Fibonacci #48.

9 days ago

Full 616-sample evidence at c1bd949 confirms isolated and independent arrays beat JSC, but shared-realm arrays remain the largest scaling problem: 1493.541 ms at 1 lane and 8451.285 ms at 8 lanes, only 1.41x scaling, with 44.30% RSD at 8 lanes.

The report remains uncommitted while this #47/#49 overlap is addressed.

9 days ago

Landed the first shared-array contention reduction in 80ff72fd (perf(vm): quicken synchronized array operations (#47)). Shared dense index reads now use a short element-lock snapshot, length uses the synchronized length helper, direct Array-prototype data reads use receiver/prototype property-lock snapshots plus seqlock IC publication, and intrinsic push takes the existing synchronized packed append path directly. Own method overrides and prototype accessors remain observable (covered by a new shared-mode regression test).

Paired 5-sample shared arrays 80 medians at the same checksum:

  • 8 lanes: ~1.01 s -> ~0.400 s (about 2.5x faster)
  • 1 lane: ~255.5 ms -> ~100.3 ms (about 2.55x faster)
  • isolated single arrays 80 stayed neutral at ~10.9 ms

A follow-up profile shows the remaining shared scaling ceiling is repeated contention on the common Array prototype lock for each push. Next I am moving the already-guarded packed-loop tier into synchronized checkpoint-bounded batches so that validation/locking is amortized without skipping VM safepoints.

9 days ago

Landed the checkpoint-bounded follow-up in 960bedc8 (perf(vm): batch synchronized packed array loops (#47)). Packed-array plans now have race-safe atomic publication in shared execution. Shared packed production/accumulation batches stop at the next 1024-step VM checkpoint and max-step boundary, validate the live Array prototype data method through the synchronized IC path, use the existing locked packed append, and hold the element lock for a bounded packed-read batch. The isolated and synchronized paths produce identical results and exact bytecode-step counts in the focused test; both packed push and sum tiers are asserted active in shared mode.

Paired 7-sample shared arrays 80 medians, same checksum:

  • 1 lane: ~96.8 ms -> ~11.4 ms (8.5x)
  • 8 lanes: ~389.6 ms -> ~28.9 ms (13.5x)
  • effective 8-lane speedup: ~1.99x -> ~3.16x

At the full jobs=450 workload (7 samples), 960bedc8 measured:

  • shared 1 lane: ~62.5 ms
  • shared 8 lanes: ~205.8 ms (about 2.43x effective scaling)
  • zig-js independent steady 8 lanes: ~120.3 ms
  • JSC independent steady 8 lanes: ~206.7 ms

So the shared-realm capability is now essentially level with JSC independent contexts at full work, while zig-js independent contexts are ~1.72x faster. Keeping #47 open because allocator/remaining shared contention still limits shared scaling well below the desired curve.

9 days ago

Accepted full evidence (42d0ec26, report ccee8842) confirms the array result: direct single 61.569 ms vs JSC 124.911 ms; independent steady 8 lanes 93.789 vs 208.772 ms; cold 8 lanes 99.306 vs 211.454 ms. Shared scaling is now 2.43x at 8 lanes (63.442 -> 208.605 ms for 8x work), materially above the old 1.71x but still the weakest shared workload, so this issue remains open for allocator/contention and broader semantic/counter coverage.

9 days ago

Latest July 14 evidence: arrays now beats JSC directly (65.839 ms vs 128.433 ms, 1.95x zig-js throughput) and at eight warmed independent contexts (105.306 ms vs 203.256 ms, 1.93x). Shared-realm scaling is 2.40x at eight lanes, up from the accepted 1.71x baseline. Publication: fdb32d8; measured commit: db131e0. Remaining unchecked profiling/counter and final TSan criteria should be reconciled before closure.

9 days ago

Completed by the published July 15 matrix: array throughput now wins every fair direct and independent-context comparison, retains 3.07x eight-lane shared scaling, and passed the documented semantic/GC/no-GIL/TSan publication gate. The remaining cross-workload shared scaling target stays tracked in #44/#97.

Sign in to comment on this issue.