Conversation
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.
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
marked this pull request as ready for review
September 20, 2026 14:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
& | ^ << >> >>> ~to native i32 instructions in the LLVM backend instead of calls to thescr_bit_*runtime helpers.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.tests/corpus/141-bitwise-inline-coercion.tspinning ToUint32 boundaries, shift-count masking, and operand evaluation order against the Node oracle, andpackages/compiler/test/bitwise-emission.test.tsasserting the emitted IR carries noscr_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,performancegovernor,--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):
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 MiBUint8Array; 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
.llgrows 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-O2that reverses in the linked binary:141-bitwise-inline-coercion.ts(new, exhaustive edge cases)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, anddifferential.test.ts— pinned to the C lane — is unaffected.Validation
pnpm -r build140-bitwise-operators.tsand141-bitwise-inline-coercion.ts: pass in both the C and LLVM differential lanesbitwise-emission.test.ts: 7/7SCRIPTC_SAN=1) LLVM lane over both bitwise corpus programs: passiso-8859-16(1423-text-codec.ts),@types/nodedrift onaddress().port(2672, which never typechecks so never reaches a backend), and child-process/stream sandbox restrictions.pnpm test:sandbox(no Vercel credentials). CI covers both lanes sharded.Fixes #357