ReviewOS

zig-utils/zig-js

Perf: beat JSC on arithmetic and property-access kernels

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

Parent: #44 Related tier-up work: #52

Accepted baseline

The July 13, 2026 symmetric report measures:

  • direct single context: JSC leads arithmetic by 3.62x and properties by 3.82x;
  • eight independent warmed contexts: JSC leads by 3.16x and 3.67x;
  • eight-lane independent scaling: zig-js reaches 5.10x and 4.74x, versus JSC at 4.60x and 4.55x;
  • zig-js shared-realm scaling: 4.91x and 4.78x.

Evidence: harness c9f68e3; 616-sample report/README 2ebda1a.

Optimization progress

Operand-stack append fast path — 22405c4

A macOS sample profile of the exact arithmetic bytecode workload attributed 1,105 of 3,403 sampled top frames (32.5%) to the generic ArrayList ensure-capacity call boundary used by operand pushes. fmod accounted for only 156 samples (4.6% including its stub); the remaining majority was bytecode dispatch/runChunk.

Reproduce the profile by starting:

/usr/bin/sample bench-comparison-zig-js 5 1 -wait -mayDie -fullPaths -file /tmp/zig-js-arithmetic.sample.txt

then running:

./zig-out/bin/bench-comparison-zig-js single arithmetic 80 20

Seven alternating old/new single-context pairs measured arithmetic 420.719 -> 359.127 ms (1.172x) and properties 324.687 -> 242.740 ms (1.338x), with unchanged checksums.

Isolated property IC mode hoist — 6033d33

The follow-up property profile exposed property mutex/TLS bookkeeping and repeated parallel-mode loads on every isolated-context IC hit. Hoisting the enable-only mode and retaining locks/seqlocks for shared/concurrent-GC contexts improved properties 243.048 -> 211.521 ms (1.149x); arithmetic stayed neutral at 1.000x. The 88-group quick matrix and focused InlineCache test passed, including the seqlock test under TSan.

Small positive-integer remainder — 464fde5

A guarded u32 remainder path avoids the out-of-line fmod call for positive integral operands while retaining the IEEE path for zero, negative, fractional, infinite, and large values. Arithmetic improved 353.621 -> 342.592 ms (1.032x); properties was neutral at 1.007x. Tests explicitly cover fractional/negative input and negative-zero preservation.

After these removals, a new 4,111-sample arithmetic profile attributes 3,862 top frames (94.0%) to runChunk and 246 (6.0%) to fmod/stub. The remaining multi-fold gap is predominantly dispatch, motivating #52 rather than benchmark-specific source shortcuts.

Investigation tracks

  • baseline native-code tier (#52)
  • VM opcode dispatch and numeric representation/conversion cost
  • modulo and integer fast paths
  • shape lookup and inline-cache hit path
  • property load/store write barriers
  • parser/evaluate overhead remaining inside the timed boundary

Acceptance criteria

  • Capture a reproducible profile and attribute the dominant costs before optimizing.
  • Add focused microbenchmarks/counters that distinguish dispatch, arithmetic, and IC behavior.
  • Land optimizations in independently reviewable commits with semantic regression tests.
  • Preserve exact Number semantics, shape transitions, prototype invalidation, GC barriers, and no-GIL safety in the landed fast paths.
  • Beat JSC with statistically stable results for both arithmetic and properties in the fair single-context matrix.
  • Beat JSC in every reported independent steady/cold row for these workloads.
  • Retain or improve independent-context and shared-realm scaling.
9 days ago

Baseline native numeric loops are now available in 27e940d (#52), including exact checkpoints and generated positive-u32 remainder. The first memory-stack emitter is deliberately recorded as performance-neutral on the arithmetic row: 347.647 ms JIT vs 347.560 ms deterministic bytecode median (80 jobs, 3 samples, identical checksum). Next arithmetic work must remove spill/load and per-bytecode accounting overhead through generic fusion/register allocation before rerunning the full publication matrix.

9 days ago

79e6ba9 moves the native numeric operand stack from memory spills into preserved FP registers. Direct arithmetic improved from 347.647 ms to 339.229 ms median (80 jobs × 3, 1.025x), now 1.025x faster than the deterministic bytecode baseline with the same checksum. The remaining large cost is per-bytecode accounting plus canonical frame-local stores; #52 will next cache non-escaping numeric locals in registers and materialize them only at safepoints.

9 days ago

095409a caches numeric locals in preserved FP registers and writes them back only at safepoints. Direct arithmetic improved another 1.038x (339.229 → 326.795 ms median); cumulative native speedup over bytecode is 1.064x with identical checksum. Per-bytecode step/checkpoint prefixes are now the dominant generated-loop overhead.

9 days ago

Arithmetic update: pushed d456d708 from #52. Batching exact native step/checkpoint accounting at numeric basic-block granularity improved the order-balanced 80-job direct arithmetic median from 375.236 ms to 362.947 ms (1.034x throughput, 10 samples per binary, identical checksum 74,803,480). Focused ARM64, exact-budget, checkpoint, fallback, and remainder semantics remain green. The accepted JSC comparison is not being replaced by this local A/B; the full symmetric matrix stays batched for a later milestone.

9 days ago

Arithmetic progress: pushed 02dc511a from #52. Fast numeric compare + jump_if_false pairs no longer materialize a boxed Boolean unless exact checkpoint/budget replay is required. Against parent d456d708, the order-balanced 160-job development gate improved from 843.174 ms to 777.378 ms median (1.085x throughput; four samples per binary; identical checksum 149,613,360). This deliberately remains local A/B evidence rather than rewriting the accepted symmetric JSC report.

9 days ago

Arithmetic update: 8af9df8f (completed #55) removes duplicate exact-accounting guards across safe hot conditional successors. Order-balanced direct medians improved 342.946 → 340.866 ms at 80 jobs (1.0061x, 10 samples/binary) and 678.785 → 675.157 ms at 160 jobs (1.0054x, 6 samples/binary), with matching checksums. This is development A/B evidence; the accepted symmetric JSC report remains unchanged until the next material batch.

9 days ago

Guarded integer JIT lowering landed in 6a1c844c via #56. Order-balanced direct-single arithmetic (80 jobs, checksum 74803480) improved from parent median 108.130 ms to 39.471 ms (2.74x). In a separate balanced set, current zig-js measured 30.763 ms versus JSC 126.951 ms, making zig-js 4.13x faster on this arithmetic row. Keeping #46 open until CI and the eventual full symmetric publication matrix confirm the batch.

9 days ago

Property workload progress from #57 landed on main in 7a541f4a.

Balanced exact-main A/B (20 samples/build, 80 jobs, checksum 120785000) improves 206.81 → 142.71 ms (1.45×) with no arithmetic regression. A same-session symmetric run is 149.67 ms zig-js vs 81.64 ms system JSC, so properties remain 1.83× slower and this issue remains open.

Detailed guards, test coverage, and ranges: #57.

9 days ago

Another property slice landed in ad547ae4 via #57. Exact parent/child ABBA improves 154.01 → 119.15 ms (1.29×) with unchanged arithmetic. Same-session properties are now 130.41 ms zig-js vs 85.59 ms JSC (1.52× gap). Keeping this issue open.

9 days ago

Cached property plans landed in 1d9d2b22 via #57. The clean exact-parent ABBA improves 146.22 → 104.70 ms (1.40×); a separate noisy window still showed a positive 1.18×. A 40-sample arithmetic guard is clean (29.74 vs 29.17 ms). Clean same-session properties are 114.00 ms zig-js vs 86.84 ms JSC, leaving ~1.31× to close.

9 days ago

Property specialization landed in 80623d3d via #57: exact-parent ABBA improves 95.99 → 84.09 ms (1.14×) with clean arithmetic. Against JSC, 20 samples slightly favored zig-js (85.29 vs 85.79 ms), while a larger 40-sample ABBA slightly favored JSC (83.40 vs 82.08 ms). Treating this as parity; keeping the issue open for a durable lead.

9 days ago

Properties now have a durable lead after 4bcbbec9 via #57. In a 40-sample symmetric run, zig-js is 75.02 ms vs 84.90 ms JSC (1.13× faster) with identical checksum 120785000. Exact-parent ABBA is 83.56 → 74.20 ms (1.13×) and arithmetic remains clean. Keeping #46 open until CI/full matrix confirms both workloads together.

9 days ago

Full 616-sample evidence at c1bd949 exposed a remaining property target despite the earlier focused win. At 80 jobs, single-context median is 101.342 ms for zig-js vs 80.330 ms for JSC, about 1.26x slower. Independent steady is also 1.03x to 1.08x slower across 1 to 8 lanes.

The accepted full report is currently regenerated locally but intentionally not published while this row loses. Reopening profiling on the exact property workload before the final publication run.

9 days ago

Property-loop follow-up landed in 33f887d1 (tracked in #57). The fair direct properties row moved from ~100.3 ms to ~23.4 ms median for zig-js, versus ~80.7 ms for system JSC (10 samples, jobs=80, identical checksum), so zig-js now wins that single-context row by ~3.45x. Shared-realm execution retains the synchronized generic path and was neutral in the paired 8-lane smoke check. I am leaving the regenerated full report unpublished until the remaining cold-start and shared-array losses are addressed and the full matrix is rerun once.

9 days ago

1c720b36 removes the remaining cold arithmetic tiering penalty: first-call numeric candidates compile eagerly, reducing cold arithmetic jobs=160 from ~809 ms to ~77.5 ms at one lane versus JSC ~237.5 ms, and to ~174.4 ms at eight lanes versus JSC ~363.4 ms. Warmed arithmetic remains neutral (~55.3 ms). The candidate filter keeps property/call-heavy first invocations off the compiler path.

9 days ago

Completed for the accepted comparison scope. The full report shows zig-js winning arithmetic and properties in direct, steady, and cold modes at every lane count while improving scaling. Direct: 55.954/56.900 ms vs JSC 230.350/192.922 ms. Steady 8: 71.166/78.794 vs 362.861/298.952 ms. Evidence: ccee8842; README 09d543c3.

Sign in to comment on this issue.