ReviewOS

zig-utils/zig-js

Perf: add guarded integer specialization to the baseline JIT

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

Parent JIT: #52 Arithmetic target: #46

Problem

After block accounting, register-resident numeric locals, compare/branch fusion, and hot-successor batching, the arithmetic kernel remained roughly 2.8x behind the then-accepted JSC direct result. Repeated A/B experiments showed that removing FP moves or constant-divisor setup was neutral: the critical path was keeping integer loop state as f64, validating it, converting it to an integer, dividing, and converting it back on every %.

Resolution

The guarded integer specialization landed as a sequence of general baseline-JIT changes:

  • 69dfc786 — unsigned entry guards before native work
  • 6011cc1b — integer provenance dataflow
  • 1fc7e9c1 — bounded integer ranges
  • 5b8aacb3 and b21cdfb7 — branch refinement and conservative threshold widening
  • 8dd91738 — checked unsigned 64-bit arithmetic
  • 61b379cc — integer register selection
  • 6a1c844c — guarded integer-region lowering

The implementation now:

  • evaluates parameter kind/range guards before native step accounting or frame-slot mutation;
  • propagates integer provenance and conservative ranges through loops, branches, and merges;
  • keeps selected locals and operand values in GPRs and materializes canonical Number words at checkpoints/returns;
  • lowers guarded integer add/sub/mul/comparisons and remainder without repeated f64 round trips;
  • rejects or falls back before observable native work when entry assumptions fail, with checked range/overflow behavior rather than wrapping;
  • retains exact bytecode step accounting, the 1,024-step checkpoint contract, termination/GC/GIL servicing, and interpreter fallback for fractional, NaN, negative-zero, escaped-frame, or unsupported shapes;
  • selects only from bytecode/runtime guards—never source strings, function names, benchmark names, or call-site identity.

Focused tests cover guard acceptance/rejection, mixed-kind provenance rejection, bounded ranges, guarded remainder execution, register encodings, exact native step limits, and non-number fallback. The new zig build test-jit small root keeps those tests directly runnable without the monolithic integration artifact.

Accepted performance evidence

The July 15, 2026 128-byte-slab comparison report measures identical arithmetic source/work across engines:

Required work

  • Add per-parameter integer/range guards evaluated before native step accounting or slot mutation.
  • Extend numeric dataflow with integer provenance and conservative ranges across loops/merges.
  • Allocate integer locals/operand values to GPRs while retaining canonical Number publication at safepoints and returns.
  • Lower integer add/sub/mul/comparisons and constant remainder without repeated f64 round trips.
  • Define checked overflow handling through proofs, guarded fallback, or rejection before observable work.
  • Preserve max-step errors, 1,024-step GIL/GC/termination checkpoints, NaN/-0/fractional fallback, and escaped-frame rejection.
  • Add focused guard/deopt/range tests plus accepted order-balanced arithmetic evidence.
  • Publish the symmetric JSC report only after the optimization batch and gates were material.
9 days ago

Pushed 281193d8 (docs: specify guarded integer tier (#56)) as the contract-first chunk. It updates the native ABI documentation to match the actual stable frame, makes precise side-exit reconstruction explicit, records pre-step entry-guard rules, conservative integer/range merge requirements, overflow choices, safepoint Number materialization, and the prohibition on benchmark/source/function-name recognition. It also corrects backend status: generated execution is currently AArch64/Darwin; x86-64 and other POSIX transitions remain future work. bun run docs:build passed (23 pages).

9 days ago

Pushed 69dfc786 (feat(jit): add unsigned entry guards (#56)). CompiledCode now carries an immutable required_u32_slots mask, and the VM evaluates it before native-frame construction, step accounting, or slot mutation. Exact guards accept canonical non-negative u32 Numbers and reject -0, negatives, fractions, NaN, infinity, 2^32, non-Numbers, and out-of-frame bits. Existing generic Number code keeps a zero mask until integer analysis/lowering opts in. Focused ReleaseFast coverage passed 2/2.

9 days ago

Implemented the architecture-neutral integer provenance lattice in 6011cc1b.

  • tracks signed, nonnegative, and positive facts across locals and the operand stack
  • weakens facts monotonically at CFG joins and reprocesses affected states
  • recognizes guarded parameters and exact integer constants without treating provenance as a finite range proof
  • handles benchmark-shaped nested loops and positive-divisor remainder provenance
  • permits mixed-kind body locals only after an unconditional overwrite; live mixed-kind reads remain unsupported

Validation (ReleaseFast):

  • integer provenance tests: 3 passed
  • numeric local loop across checkpoint: 2 passed (including root import test)

This is analysis infrastructure only; GPR lowering remains gated on a separate conservative range proof.

9 days ago

Added conservative safe-integer interval analysis in 1fc7e9c1.

  • guarded integer parameters begin in [0, UINT32_MAX]
  • exact constants and add/sub/mul propagate bounds only inside JavaScript’s safe-integer interval
  • positive-divisor remainder recovers a finite result bound even when the dividend interval widened away
  • expanding loop backedges widen intervals to unknown immediately, preventing analysis-time iteration proportional to runtime loop counts
  • provenance and interval proofs remain metadata only; generic FP emission is unchanged

ReleaseFast integer-analysis group: 4 passed, including the nested arithmetic benchmark shape, straight-line guarded remainder, and the mixed-kind rejection boundary.

9 days ago

Added branch-sensitive range refinement in 5b8aacb3.

Numeric comparisons now retain conservative operand facts and stable direct-local origins through CFG joins. jump_if_false refines both successor states for <, <=, >, >=, equality, and inequality without touching unknown/non-integer comparisons. This lets a widened induction variable recover a finite body-path bound from its loop condition; the benchmark-shaped nested loop now proves i is [0, 99_999] inside i < 100_000.

ReleaseFast integer-analysis group: 4 passed. Emission remains unchanged until the complete region has safe bounds and entry guards.

9 days ago

Added threshold-based loop widening in b21cdfb7. Exact safe-integer constants plus the unsigned-entry ceiling form a sorted widening set, so expanding backedges jump between finitely many program-derived bounds instead of iterating with the runtime trip count or immediately losing every interval.

The nested arithmetic shape now proves i is [0, 100_000] at the loop test, [0, 99_999] in the body, and the dividend entering % 1_000_003 stays non-negative and within the safe-integer interval. Focused Debug coverage passed; the next ReleaseFast run will be batched with emitter work.

9 days ago

Added the AArch64 primitives for the guarded safe-integer path in 8dd91738: 64-bit subtract, multiply, unsigned divide/remainder components, and f64/u64 conversions. Encodings were cross-checked against Clang’s AArch64 assembler; all 5 standalone backend tests pass in ReleaseFast. This is an emitter prerequisite only and does not alter code selection.

9 days ago

Added architecture-neutral GPR selection and representation dataflow in 61b379cc. The pass ranks only locals whose reachable loads/stores are proven non-negative safe integers, caps selection at four callee-saved registers, intersects local representation at merges, rejects inconsistent operand-stack merges, and iteratively removes any local that could be loaded after losing its integer representation.

On the nested arithmetic kernel it selects four locals and proves both % operands are GPR-resident. Focused Debug coverage passed. No runtime emission changes yet.

9 days ago

Implemented guarded GPR lowering in 6a1c844c.

  • selected non-negative safe-integer locals use x23/x26–x28; operands use x0–x7
  • guarded add/sub/mul, unsigned comparisons, and positive-divisor remainder stay in 64-bit GPRs
  • mixed operations convert to the existing FP path on demand
  • safepoints publish canonical Number words and reload caller-saved integer operands
  • returns materialize exact Numbers; u32 parameter masks are checked by the VM before native steps or mutation
  • selections are removed unless their numeric values remain publishable at every reachable state

Validation:

  • ReleaseFast integer group: 5/5 passed
  • Debug numeric baseline tier/fallback/accounting test: passed
  • direct nested remainder execution passed at checkpoint distances 1, 17, and 1024

Order-balanced direct-single arithmetic evidence (80 jobs, exact checksum 74803480):

  • parent 61b379cc: 20 samples, median 108.130 ms (82.605–113.532 ms)
  • candidate: 20 samples, median 39.471 ms (32.710–45.129 ms), 2.74x faster
  • separate current JSC set: 20 samples, median 126.951 ms (120.733–137.077 ms)
  • paired candidate set: 20 samples, median 30.763 ms (28.845–32.559 ms), zig-js 4.13x faster than JSC

The accepted 616-sample report remains unchanged until the optimization batch is ready for one full rerun.

Sign in to comment on this issue.