ReviewOS

zig-utils/zig-js

Perf: beat JSC on arithmetic and property-access kernels

#46
Closed chris opened this 10 days ago ·
10 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.
10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.

10 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.