ReviewOS

zig-utils/zig-js

Perf: add a baseline native-code tier for hot bytecode functions

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

Parent: #44 Successors: #132 (native property regions), #133 (native packed-array regions) Related completed work: #46, #47, #48, #53, #56

Completed baseline

The general baseline native-code tier is implemented and documented in https://github.com/zig-utils/zig-js/blob/main/docs/baseline-jit.md. It includes race-safe tier publication, context-owned W^X executable mappings, a stable native frame ABI, guarded numeric/control-flow lowering, register-resident values, exact step/checkpoint/GC semantics, deterministic fallback, and Context.Options.enable_jit = false for differential validation.

The July 15 complete matrix beats JSC in all directly comparable rows. The saved July 16 symbolized profile now proves the execution boundary:

  • arithmetic: 93.3% of reported leaves in generated MAP_JIT code; zero residual runChunk leaves;
  • properties: guarded VM kernels plus 36.9% residual runChunk leaves;
  • arrays: 65.3% residual runChunk leaves plus dense-array runtime helpers;
  • recursive Fibonacci: 96.6% in the guarded observable recurrence kernel; zero residual runChunk leaves.

Profile: https://github.com/zig-utils/zig-js/blob/main/docs/.data/baseline-jit-profile-2026-07-16.md Publication: https://github.com/zig-utils/zig-js/blob/main/docs/.data/benchmark-comparison-2026-07-15-128-byte-slab.md

Broader native coverage is now split into evidence-specific successors #132 and #133 instead of leaving the completed baseline milestone indefinitely open.

Acceptance

  • Document native frame layout, Value representation, calling convention, safepoints, exception transfer, and invalidation.
  • Add native-on/off differential tests for results, fallback, exact steps, and visible effects.
  • Reject unsupported property/array opcodes rather than executing stale assumptions.
  • Preserve precise GC/checkpoints, stack limits, closures/upvalues, arguments, eval, generators/async fallback, and no-GIL ownership.
  • Keep unsupported functions on deterministic bytecode fallback.
  • Pass focused semantic, full GC/no-GIL, and suppression-free TSan validation.
  • Save symbolized profiles separating generated code, VM quickening/runtime kernels, and residual dispatch.
  • Beat JSC in every directly comparable benchmark row with the sibling workload work.
  • Document build/runtime support and expose a deterministic disable switch.
9 days ago

Design contract landed in 2ddb592 (docs: design baseline native tier (#52)).

It fixes the tier state/publication model, stable entry ABI, exact side-exit state, first numeric/local/control subset, safepoint/step/GC obligations, W^X policy, shared-chunk ownership, and differential/performance gates before executable code is added.

9 days ago

Executable-memory foundation landed in 3e69e9c (feat(jit): add executable code memory (#52)).

Implemented Darwin AArch64/x86-64 MAP_JIT allocation, thread-local write protection, instruction-cache invalidation, immutable publication, an executing native smoke test, and an unsupported-target fallback compile check. Focused ReleaseFast tests: 3/3 passed.

9 days ago

Race-safe per-chunk tier claims landed in fb3734d (feat(jit): add race-safe tier claims (#52)).

A chunk now owns atomic cold/compiling/ready/rejected state and entry hotness. Contending entrants never wait; exactly one compiler claims the chunk and unsupported compilation is cached. ReleaseFast focused tests passed 4/4, including an 8-thread claim race; x86-64 Linux compile-only fallback also passed.

9 days ago

First real bytecode-to-native tier landed in 2a88b8a (feat(jit): tier constant bytecode functions (#52)).

Primitive constant-return chunks now lower through the architecture-neutral selector and AArch64 entry backend after three entries. The VM preserves exact executed-bytecode step counts, falls back at 1024-step termination/GIL/GC checkpoint boundaries, rejects embedded GC pointers, caches unsupported chunks, and works through both direct execution and the heap activation trampoline.

Validation: focused ReleaseFast native integration passed; constant compiler test passed; the complete 88-group quick JSC comparison matrix passed with stable cross-engine checksums. No 616-sample publication run was spent because this correctness slice does not yet cover the benchmark kernels.

9 days ago

Numeric-frame foundation landed in 9987a72 (feat(jit): expose numeric frame state (#52)). Plain function chunks now publish parameter/local arity, and the stable native ABI exposes rooted slots, numeric spill storage, exact step state, and runtime checkpoint/remainder callbacks without depending on Zig-private layouts. Batched ReleaseFast native validation: 16/16 passed, 0 leaked.

9 days ago

Generic numeric/control-flow baseline lowering landed in 27e940d (feat(jit): lower numeric bytecode loops (#52)). It adds abstract bytecode validation, arm64 code generation for primitive constants/locals/arithmetic/comparisons/branches/returns, exact per-bytecode budget and 1024-step checkpoint accounting, GC/GIL/termination runtime checkpoints, guarded positive-u32 % with full IEEE fallback, numeric entry guards, and Context.Options.enable_jit = false for deterministic differential runs. Validation: ReleaseFast numeric suite 8/8, arm64 encodings 4/4, VM tier/fallback suite 2/2, all zero leaks; the quick symmetric matrix stayed checksum-clean. Direct arithmetic A/B at 80 jobs × 3 samples was performance-neutral: JIT median 347.647 ms vs disabled 347.560 ms (1.000x), after fixing the initial runtime-remainder regression. This is correctness scaffolding, not the claimed performance win; register-resident/fused lowering remains next.

9 days ago

Register-resident numeric operand stacks landed in 79e6ba9 (perf(jit): keep numeric stack in registers (#52)). Generated entries now use ABI-preserved d8-d15 values across exact runtime checkpoints and materialize canonical NaN-box words only at frame stores/returns. ReleaseFast validation: arm64 5/5 and numeric 9/9, zero leaks. Same direct arithmetic runner, 80 jobs × 3: median 339.229 ms vs 347.647 ms immediately before (1.025x faster) and 347.560 ms with JIT disabled (1.025x faster), identical checksum.

9 days ago

Numeric local register caching landed in 095409a (perf(jit): cache numeric locals in registers (#52)). Non-escaping numeric frame slots now remain in d8-d15; cold checkpoint islands canonicalize/write them into rooted frame slots before GC, budget, termination, or error work and preserve caller-saved live operands across callbacks. ReleaseFast numeric validation: 9/9, zero leaks. Direct arithmetic median improved 339.229 → 326.795 ms (1.038x); cumulative vs deterministic bytecode is 1.064x, identical checksum.

9 days ago

Landed and pushed d456d708 (perf(jit): batch numeric block accounting (#52)).

The numeric tier now builds verified bytecode basic blocks and accounts for budget/checkpoint counters once per hot block. If the exact evaluation limit or 1,024-step safepoint falls inside a block, generated code enters a cold instruction-by-instruction replay and rejoins the fast CFG afterward. ARM64 gained exact immediate compare/subtract encodings for those guards.

Validation:

  • ReleaseFast AArch64 focused filter passed.
  • ReleaseFast vm: numeric baseline tier preserves steps and non-number fallback passed, including exact max-step+1 RangeError and first-op checkpoint coverage.
  • ReleaseFast compiler executes a numeric local loop across a checkpoint passed.
  • Order-balanced direct arithmetic gate, 80 jobs / 10 samples per binary: new median 362.947 ms versus current-main predecessor 375.236 ms, or 1.034x throughput. Both produced checksum 74,803,480.

The full 616-sample publication matrix remains intentionally deferred until a larger optimization batch.

9 days ago

Pushed 02dc511a (perf(jit): fuse numeric loop branches (#52)).

Checkpoint-free fast blocks now fuse a numeric comparison immediately consumed by jump_if_false into one FP compare and one conditional branch. This removes boxed-Boolean construction plus scratch store/reload/bit-test traffic from loop conditions. Boundary blocks deliberately retain the materialized form because a runtime checkpoint may clobber condition flags.

Validation:

  • ReleaseFast numeric-tier test passed with new true/false/equal/NaN coverage.
  • The test forces checkpoint replay at each early instruction distance and verifies the fused function remains exact.
  • Order-balanced longer-signal direct arithmetic A/B against parent d456d708, 160 jobs / 4 samples per binary: 777.378 ms candidate median versus 843.174 ms predecessor, or 1.085x throughput. Candidate range 768.641–780.654 ms; predecessor 817.293–915.899 ms; checksum 149,613,360 for both.

The 160-job run is a development A/B chosen to reduce host noise, not a replacement for the accepted symmetric 80-job publication matrix.

9 days ago

Pushed 8af9df8f from completed #55. The numeric tier can now pre-account a conditional block plus its sole fallthrough successor, enter the successor without a duplicate seven-instruction guard, refund its bytecode count on the false edge, and retain a separately guarded entry for slow replay/transfers. Checkpoint distances 1-47 and exact VM budget semantics pass. Direct arithmetic improved 1.0061x at the official 80-job gate and 1.0054x at the 160-job longer-signal gate versus 02dc511a.

9 days ago

CI follow-up: pushed d37079e5 (test(jit): skip native execution off arm64 (#52)). The failed 02dc511a run had exactly two root causes duplicated across normal/TSan shards: native constant-entry execution tests expected .ready on Linux x86_64 even though the only emitted backend is currently AArch64 and correctly returns UnsupportedTarget. Both execution-only tests now skip off supported AArch64; architecture-neutral JIT tests remain cross-platform. Local ReleaseFast constant filter passed 3/3 on AArch64. Failure evidence: https://github.com/zig-utils/zig-js/actions/runs/29292590584.

9 days ago

Cold-entry follow-up landed in 1c720b36 (tracked by #59): an allocation-free candidate filter lets supported numeric chunks enter the baseline tier on their first call while unsupported opcode families retain the ordinary hot threshold. Cold arithmetic jobs=160 improved from ~809 ms to ~77.5 ms at one lane, with warmed throughput unchanged and exact step/checkpoint tests passing.

9 days ago

The final missing evidence is on main in b3dec852: https://github.com/zig-utils/zig-js/blob/main/docs/.data/baseline-jit-profile-2026-07-16.md

The profiles confirm generated arithmetic dispatch removal and identify the remaining property/array work without conflating it with this completed baseline milestone. Opened #132 and #133 as targeted successors. Closing #52 with every acceptance criterion satisfied.

Sign in to comment on this issue.