Skip to content

perf(llvm): inline bitwise operators as native i32 operations - #358

Open
redvulps wants to merge 1 commit into
vercel-labs:mainfrom
redvulps:perf/llvm-inline-bitwise
Open

redvulps wants to merge 1 commit into
vercel-labs:mainfrom
redvulps:perf/llvm-inline-bitwise

Conversation

@redvulps

Copy link
Copy Markdown

Summary

  • Lower & | ^ << >> >>> ~ to native i32 instructions in the LLVM backend instead of calls to the scr_bit_* runtime helpers.
  • Convert operands through emitBytesU32, the JS-exact ToUint32 conversion the typed-array store path already uses, so NaN, the infinities, and out-of-range doubles convert exactly as before; mask shift counts to five bits, since LLVM treats a shift past the operand width as poison while JavaScript wraps it.
  • Add tests/corpus/141-bitwise-inline-coercion.ts pinning ToUint32 boundaries, shift-count masking, and operand evaluation order against the Node oracle, and packages/compiler/test/bitwise-emission.test.ts asserting the emitted IR carries no scr_bit_* call while retaining the guarded slow path.

Why

Every bitwise operation compiled to an out-of-line call, so integer hot loops paid call overhead per operation and LLVM could not see through the helper to fold anything. Inlining is what removes the coercions rather than just the call: a value a bitwise operation just produced is already an exact int32, so the guarded conversion in front of the next operation folds away and a chain of them becomes straight-line integer code.

Measurements

Workload: 1,000,000 dependent xorshift32 steps (v ^= v << 13; v ^= v >>> 17; v ^= v << 5, each step >>> 0). Ryzen 9 7900X, pinned logical CPU, performance governor, --optimization release, Node 24.12.0. 11 samples per case, batches calibrated to ~150 ms, ~250 ms warmup in a fresh process per sample, shuffled order, every batch checksum verified against an independent reference.

Median ms per operation (p10/p90):

Case Node 24 scriptc 0.1.3 this branch
xorshift 1.148 (1.140/1.150) 88.878 (88.316/95.248) 1.174 (1.168/1.187)

That is 75.7x faster than 0.1.3, moving the ratio against Node from 77.4x to 1.02x. Disassembly of the benchmark's xorshift closure shows 17 scr_bit_* calls on 0.1.3 and none on this branch.

A byte-processing loop of the shape #93 describes ((x << 3) ^ (x >>> 2) plus a mask, through a 1 MiB Uint8Array; 3 runs, indicative rather than the protocol above) goes from 66.01 to 3.22 ms per round — 20.5x, on top of what #97 already fixed.

The suite's other kernels contain no bitwise operators, so they act as controls for collateral effects rather than as evidence about this path. All move within noise: fib +0.09%, mandelbrot +0.24%, dot -1.22%, sum_64k -0.07%, sum_16m -0.37%.

Emitted .ll grows before optimization — across 406 corpus programs total IR is +1.17%, with 397 byte-identical and 9 (the ones actually using bitwise operators) larger. After -O2 that reverses in the linked binary:

baseline this branch delta
8 corpus programs using bitwise ops 758,088 757,152 -936 B
141-bitwise-inline-coercion.ts (new, exhaustive edge cases) 54,624 58,392 +3,768 B

8 of the 9 shrink; the one that grows is the new edge-case corpus program itself.

Deliberate boundaries

Only the LLVM backend changes. The C backend keeps the scr_bit_* helpers, and differential.test.ts — pinned to the C lane — is unaffected.

Validation

  • pnpm -r build
  • 140-bitwise-operators.ts and 141-bitwise-inline-coercion.ts: pass in both the C and LLVM differential lanes
  • bitwise-emission.test.ts: 7/7
  • Sanitized (SCRIPTC_SAN=1) LLVM lane over both bitwise corpus programs: pass
  • Full LLVM differential corpus: 1127/1202 claimed, 92 failures — the identical failure set to the same tree with this commit reverted, so no regression. Those failures are environmental on this host: Node lacking ICU data for iso-8859-16 (1423-text-codec.ts), @types/node drift on address().port (2672, which never typechecks so never reaches a backend), and child-process/stream sandbox restrictions.
  • LLVM tier membership across all 444 corpus entries containing bitwise-operator tokens, compared against the reverted tree: 406/406 claimed, 38/38 refused, zero per-program differences, identical refusal kinds.
  • Not run locally: the full sanitized suite and pnpm test:sandbox (no Vercel credentials). CI covers both lanes sharded.

Fixes #357

The LLVM backend lowered every bitwise operator to an out-of-line
scr_bit_* runtime call. Integer hot loops pay that call on every
operation, and it dominates their runtime: a one-million-step xorshift32
measured 88.7 ms per operation, against 1.12 ms for the same algorithm in
Rust and under Node.

Emit the operations inline instead. Operands convert through
emitBytesU32, the JS-exact ToUint32 conversion the typed-array store path
already uses, the operation runs as a native i32 instruction, and the
result returns to f64 -- uitofp for >>>, sitofp for the rest. Shift counts
mask to five bits: LLVM treats a shift past the operand width as poison,
while JavaScript wraps it.

Inlining is what makes the coercions disappear. A value that a bitwise
operation just produced is already an exact int32, so the guarded
conversion in front of the next operation folds away and a chain of them
becomes straight-line integer code. The same xorshift kernel drops to
1.15 ms per operation, within three percent of Rust, and the linked
binary is 360 bytes smaller.

Semantics are unchanged. The conversion keeps its guarded slow path, so
NaN, the infinities, and values outside the modular fast range still
convert exactly as before; an unconditional fptosi would turn valid
JavaScript inputs into poison.

Only the LLVM backend changes. The C backend keeps the scr_bit_* helpers.

Measurements are one kernel on one machine (Ryzen 9 7900X, LLVM release
build), not a general speedup claim.
@vercel

vercel Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@redvulps is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@redvulps
redvulps marked this pull request as ready for review September 20, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bitwise operators lower to out-of-line scr_bit_* calls — 77x slower than Node on integer hot loops

1 participant