Skip to content

perf(codegen): inline non-BigInt bitwise ops instead of js_dynamic_bit* calls - #6860

Merged
proggeramlug merged 3 commits into
mainfrom
perf/inline-nonbigint-bitwise
Jul 27, 2026
Merged

perf(codegen): inline non-BigInt bitwise ops instead of js_dynamic_bit* calls#6860
proggeramlug merged 3 commits into
mainfrom
perf/inline-nonbigint-bitwise

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Kill the f64-bitwise → js_dynamic_bit* runtime-call cascade in hot integer
kernels. bcryptjs's _encipher Feistel network runs r ^= n ^ P[i] per round,
but the accumulators l/r are Any-typed (the let l = lr[off] inference
does not propagate the Int32Array element type), so every ^/&/>>/>>>
on them failed the is_numeric_expr check and bailed to the BigInt-aware
js_dynamic_bit* runtime helper — one opaque call per round. Post-opt -O3,
_encipher contained 12 @js_dynamic_* bitwise calls (4 bitxor, 4 shr,
2 ushr, 2 bitand).

This lowers the six bitwise ops (& | ^ << >> >>>) inline
(ToInt32 <op> ToInt32 + sitofp) whenever both operands are provably not a
BigInt, reusing the existing all-numeric inline machinery — even when the
operands are not statically is_numeric_expr.

How

  • New predicate type_analysis::is_provably_not_bigint(ctx, e) — true for a
    value whose ToNumeric can never yield a BigInt: numerics, booleans, strings,
    null/undefined/typeof/void/comparisons, numeric typed-array element
    reads, arithmetic/bitwise results (a BigInt result needs both operands
    BigInt), and locals proven non-BigInt.
  • New flow analysis collectors::not_bigint_locals (greatest fixpoint): an
    Any-typed local is non-BigInt when every assignment to it (init + every
    LocalSet, including inside closure bodies) is a non-BigInt expression. This
    is what proves bcryptjs's l/r (init from an Int32Array element, then
    only ever ^=/+=-updated) are never a BigInt. Wired onto FnCtx as a
    per-region fact (RepresentationFacts::not_bigint_locals).
  • expr/binary.rs: before the BigInt bail, for the six bitwise ops only, if
    both operands is_provably_not_bigint, fall through to the inline path.
  • Gated behind PERRY_INLINE_NONBIGINT_BITWISE (default on; =0/off/false
    reverts to the runtime call). Keyed into the object cache like the sibling
    codegen env flags.

Correctness

  • BigInt bail preserved. If either operand could be a BigInt the op still
    routes to js_dynamic_bit*, so a mixed bigint <op> number still throws
    TypeError and bigint <op> bigint still computes a BigInt. The
    is_provably_not_bigint gate requires both operands to pass, and the
    existing is_bigint_expr guard already caught statically-BigInt operands
    upstream.
  • NaN-safe. The inline path keeps the guarded toint32_wrap for any operand
    not proven finite — an out-of-bounds typed-array read is undefined/NaN
    (still non-BigInt), and toint32_wrap maps NaN/±Inf to 0 per spec.
    toint32_fast is used only where is_known_finite already proves finiteness.
  • Sound flow set. not_bigint_locals is a greatest fixpoint that only ever
    removes locals on a possibly-BigInt write; it may under-approximate but
    never over-approximates. Declared types are trusted exactly as
    is_numeric_expr already trusts them.

Results (reproduction: a faithful _encipher, 2.1M calls)

  • @js_dynamic_bitxor in optimized _encipher: 4 → 0 (and all 12
    js_dynamic_* bitwise calls → 0; inline xor/and/lshr i32 now present).
  • enc.ts min-of-5: 3651 ms → 1845 ms (1.98x); Node 26.5.0 ~135 ms. Still
    ~13.7x Node — the per-read typed-array guard is still un-hoisted (a separate
    follow-up); the win here is purely from removing the per-round runtime call.
  • Byte-exact vs Node (lr0=2135713266 lr1=-1949122846), flag on and off.

Tests

  • test-files/test_gap_inline_nonbigint_bitwise.ts (byte-exact vs Node):
    typed-array-read operands, untyped-any element reads, an out-of-bounds
    undefined operand, and BigInt operands (bigint^bigint computes a BigInt;
    bigint <op> number throws TypeError). Verified byte-identical flag on,
    flag off, and under PERRY_GC_FORCE_EVACUATE=1.
  • object_cache_tests::key_changes_with_codegen_env_vars extended to cover
    PERRY_INLINE_NONBIGINT_BITWISE.

Summary by CodeRabbit

  • New Features

    • Added an inline fast path for & | ^ << >> >>> when both operands are provably non-BigInt, improving performance in integer-heavy workloads.
    • Introduced PERRY_INLINE_NONBIGINT_BITWISE to enable/disable the optimization (disabled when set to 0).
  • Bug Fixes

    • Preserved correct JavaScript BigInt semantics, including required TypeError for mixed BigInt/number bitwise operations.
    • Ensured compiled output cache keys change when the optimization flag changes.
  • Tests

    • Expanded coverage for typed-array/undefined/nested cases and BigInt mixed-type throwing behavior.

Ralph Küpper added 2 commits July 26, 2026 21:36
…t* calls

bcryptjs _encipher's Feistel `^` lowered to a js_dynamic_bitxor runtime call
per round: the accumulators l/r are Any-typed (the `let l = lr[off]` inference
does not propagate the Int32Array element type), so the bitwise-op lowering
failed the is_numeric_expr check and bailed to the BigInt-aware dynamic helper
before reaching the inline ToInt32+xor path. Post-opt -O3, _encipher held 12
js_dynamic_* bitwise calls (4 bitxor, 4 shr, 2 ushr, 2 bitand).

For the six bitwise ops, when BOTH operands are provably not a BigInt, lower
inline (ToInt32 <op> ToInt32 + sitofp) reusing the existing all-numeric path:

- New predicate type_analysis::is_provably_not_bigint (Binary/Unary handled
  structurally, so anyA ^ anyB — possibly bigint^bigint — is not misclassified).
- New flow set collectors::not_bigint_locals (greatest fixpoint): an Any-typed
  local is non-BigInt when every assignment to it is a non-BigInt expression.
  Wired onto FnCtx as RepresentationFacts::not_bigint_locals.

The BigInt bail is preserved: a mixed bigint/number op still throws TypeError
and bigint^bigint still computes a BigInt (both operands must pass the gate).
The inline path keeps the NaN-safe guarded toint32_wrap for operands not proven
finite (an out-of-bounds typed-array read is undefined/NaN, still non-BigInt).
Gated behind PERRY_INLINE_NONBIGINT_BITWISE (default on; =0 reverts); keyed into
the object cache.

Optimized _encipher: 12 js_dynamic_* bitwise calls -> 0. enc.ts min-of-5
3651ms -> 1845ms (Node 26.5.0 ~135ms; the un-hoisted per-read guard is a
separate follow-up). Byte-exact vs Node, flag on and off.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds flow analysis for locals that cannot hold BigInt, propagates those facts through code-generation contexts, and uses them to inline six bitwise operators when enabled. Object-cache keys and runtime tests cover the new environment toggle and BigInt semantics.

Changes

Non-BigInt Bitwise Lowering

Layer / File(s) Summary
Collect non-BigInt locals
crates/perry-codegen/src/collectors/*, crates/perry-codegen/src/collectors/hir_facts.rs
Adds a fixed-point write analysis for identifying locals that are provably non-BigInt and stores the resulting set in TypeFacts.
Thread facts through compilation contexts
crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/codegen/{closure,entry,function,method}.rs
Adds not_bigint_locals to FnCtx and initializes it for closures, functions, methods, and module entry bodies.
Select guarded inline bitwise lowering
crates/perry-codegen/src/type_analysis/{numeric.rs,../type_analysis.rs}, crates/perry-codegen/src/expr/binary.rs
Adds expression-level non-BigInt analysis and skips the dynamic BigInt helper for eligible bitwise and shift operations when the environment flag permits it.
Cache invalidation and runtime coverage
crates/perry/src/commands/compile/object_cache*, test-files/test_gap_inline_nonbigint_bitwise.ts, changelog.d/6860-inline-nonbigint-bitwise.md
Includes the toggle in object-cache keys and tests typed-array, undefined, BigInt, mixed-operand, update, and nested-expression behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HIR
  participant TypeFacts
  participant FnCtx
  participant BinaryLowering
  participant RuntimeHelper
  HIR->>TypeFacts: collect non-BigInt local facts
  TypeFacts->>FnCtx: provide not_bigint_locals
  FnCtx->>BinaryLowering: analyze bitwise operands
  BinaryLowering->>BinaryLowering: emit inline ToInt32/ToUint32 lowering
  BinaryLowering->>RuntimeHelper: use dynamic BigInt-aware helper when proof is unavailable
Loading

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main codegen optimization in this PR.
Description check ✅ Passed The description covers the summary, changes, correctness, results, and tests, though it omits the template's related issue and checklist sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/inline-nonbigint-bitwise

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry-codegen/src/collectors/not_bigint_locals.rs (1)

35-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider dedicated unit tests for this fixed-point algorithm.

The greatest-fixpoint write analysis here (and its self-referential short-circuit in expr_not_bigint's Binary arm) is subtle: e.g. l = l ^ x is judged safe by short-circuiting on the self-reference, which is only sound because a mixed BigInt/Number bitwise op throws rather than silently coercing (so l never actually receives a bad value when the op would produce one). This reasoning is correct as written, but a future edit to expr_not_bigint (or to JS bitwise-op throw semantics assumptions) could silently break it without any test catching the regression. A few targeted #[cfg(test)] cases directly on collect_not_bigint_locals (self-referential accumulator, mutual-reference locals, let x; with no init, BigInt-literal write invalidating the local) would materially de-risk future changes to this soundness-critical pass.

Also applies to: 92-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/collectors/not_bigint_locals.rs` around lines 35 -
86, Add targeted #[cfg(test)] unit tests for collect_not_bigint_locals covering
a self-referential accumulator, mutually referencing locals, an uninitialized
let x;, and a BigInt-literal write. Assert each case’s expected membership in
the returned HashSet, keeping the tests focused on the greatest-fixpoint
behavior and expr_not_bigint Binary self-reference handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/type_analysis/numeric.rs`:
- Around line 335-375: Update is_provably_not_bigint to handle Expr::Update
before the is_numeric_expr shortcut, using the update target’s
flow-analysis/type information so increments and decrements of BigInt locals are
not classified as definitively non-BigInt. Preserve the existing behavior for
updates proven non-BigInt, and add coverage in
test_gap_inline_nonbigint_bitwise.ts for increment/decrement on a BigInt local
in a mixed bitwise expression.

---

Nitpick comments:
In `@crates/perry-codegen/src/collectors/not_bigint_locals.rs`:
- Around line 35-86: Add targeted #[cfg(test)] unit tests for
collect_not_bigint_locals covering a self-referential accumulator, mutually
referencing locals, an uninitialized let x;, and a BigInt-literal write. Assert
each case’s expected membership in the returned HashSet, keeping the tests
focused on the greatest-fixpoint behavior and expr_not_bigint Binary
self-reference handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df527325-431e-4146-acbb-faed000f7801

📥 Commits

Reviewing files that changed from the base of the PR and between 563c359 and b49d15f.

📒 Files selected for processing (15)
  • changelog.d/6860-inline-nonbigint-bitwise.md
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/not_bigint_locals.rs
  • crates/perry-codegen/src/expr/binary.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/type_analysis.rs
  • crates/perry-codegen/src/type_analysis/numeric.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/object_cache/object_cache_tests.rs
  • test-files/test_gap_inline_nonbigint_bitwise.ts

Comment thread crates/perry-codegen/src/type_analysis/numeric.rs
…bigint++)

CodeRabbit review on #6860. `(bigintLocal++) & 5` computed garbage instead of
throwing the spec TypeError (mixed bigint/number), and `(bigintLocal++) * 2`
silently produced NaN. Two-part fix:

1. is_numeric_expr(Expr::Update) was unconditionally `true`. A pre/post inc/dec
   is `ToNumeric(x) ± 1` — a Number, EXCEPT when x is a BigInt (stays BigInt).
   So it is NOT unconditionally numeric: mirror the LocalGet arm (Number/Int32
   declared type) and additionally honor the integer-range collectors
   (integer_locals / unsigned_i32_locals) so an Any-typed loop counter proven
   to hold an i32 keeps the numeric fast path. An Any local not proven integer
   (could be BigInt) is excluded — so a bigint-local Update operand no longer
   takes the all-numeric operand path (which toint32's the BigInt to 0).

2. is_provably_not_bigint gained an explicit Expr::Update { id, .. } arm before
   the is_numeric_expr shortcut, mirroring LocalGet(id) (not_bigint_locals /
   integer_locals / unsigned_i32_locals). With (1) making l_prim false for a
   bigint Update, the bitwise lowering now reaches this gate, which returns
   false, bailing to the BigInt-aware dynamic helper → throws / computes bigint.

Gap-test cases (byte-exact vs Node, flag on/off, one under
PERRY_GC_FORCE_EVACUATE=1): (b++)&num / (b--)|num / (b++)>>1 on a BigInt local
throw TypeError; (b++)^bigint computes a BigInt (9n).

Unit tests on collect_not_bigint_locals: self-referential accumulator,
mutually-referencing locals, uninitialized let, BigInt-literal write
invalidation, and taint propagation through a copy.

Lever unchanged: optimized _encipher still has 0 js_dynamic_* bitwise calls;
enc.ts still byte-exact (lr0=2135713266 lr1=-1949122846). integer-counter
arithmetic keeps the numeric fast path via integer_locals, so this is
output-preserving for numbers (a number-holding Any Update just gains a
semantics-preserving coerce).
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Thanks for the review — both points addressed, and the correctness bug turned out to need one more piece than the diagnosis suggested.

1. Expr::Update correctness bug. Confirmed. Investigating it, the observable misbehavior ((bigintLocal++) & 5 computing garbage, and (bigintLocal++) * 2 silently producing NaN) is actually driven upstream of is_provably_not_bigint: is_numeric_expr(Expr::Update) was unconditionally true (pre-existing), so a bigint-local x++ made l_prim true and the operand took the all-numeric operand-lowering path (which toint32s the BigInt to 0) — is_provably_not_bigint was never consulted for it. So I fixed it in two places:

  • is_numeric_expr(Expr::Update { id, .. }): no longer unconditionally true. A pre/post inc/dec is ToNumeric(x) ± 1 — a Number unless x is a BigInt (then it stays BigInt). Now mirrors the LocalGet arm (Number/Int32 declared type), plus honors integer_locals/unsigned_i32_locals so an Any-typed loop counter proven to hold an i32 keeps the numeric fast path. An Any local not proven integer (could be BigInt) is excluded.
  • is_provably_not_bigint: added the explicit Expr::Update { id, .. } arm you asked for, before the is_numeric_expr shortcut, mirroring LocalGet(id) (not_bigint_locals/integer_locals/unsigned_i32_locals). With the first change making l_prim false for a bigint Update, the bitwise lowering now reaches this gate, returns false, and bails to the BigInt-aware helper.

Result (byte-exact vs Node, flag on/off, one under PERRY_GC_FORCE_EVACUATE=1): (b++)&num / (b--)|num / (b++)>>1 on a BigInt local now throw TypeError; (b++)^bigint computes a BigInt (9n). Gap-test cases added to test_gap_inline_nonbigint_bitwise.ts.

This is output-preserving for numbers — integer counters keep the numeric fast path via integer_locals; a number-holding Any Update just gains a semantics-preserving js_number_coerce.

2. Unit tests for the fixpoint. Added #[cfg(test)] mod tests on collect_not_bigint_locals: self-referential accumulator (l = l ^ x → stays non-BigInt), mutually-referencing locals, an uninitialized let x;, a BigInt-literal write invalidating the local, and BigInt taint propagating through a plain copy across the fixpoint.

Re-verified: optimized _encipher still has 0 js_dynamic_bitxor (opt -O3 on the trace); enc.ts still byte-exact (lr0=2135713266 lr1=-1949122846); the 11 bitwise/bigint/int32/typed-array blast-radius gap tests all still byte-exact; cargo fmt --all -- --check clean.

(The lint failure is the known benchmark-input-freshness false-positive, handled at merge.)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/type_analysis/numeric.rs`:
- Around line 392-395: Update the Expr::Update branch in the numeric proof to
also recognize locals whose declared types are Number or Int32, mirroring the
declared-type check used by is_numeric_expr around lines 129-132. Preserve the
existing not_bigint_locals, integer_locals, and unsigned_i32_locals checks while
ensuring declared numeric locals qualify for the inline bitwise path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18d1dfb1-a121-49fa-b6b6-6509a5caa343

📥 Commits

Reviewing files that changed from the base of the PR and between b49d15f and 7e4d56c.

📒 Files selected for processing (3)
  • crates/perry-codegen/src/collectors/not_bigint_locals.rs
  • crates/perry-codegen/src/type_analysis/numeric.rs
  • test-files/test_gap_inline_nonbigint_bitwise.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test-files/test_gap_inline_nonbigint_bitwise.ts
  • crates/perry-codegen/src/collectors/not_bigint_locals.rs

Comment on lines +392 to +395
Expr::Update { id, .. } => {
return ctx.not_bigint_locals.contains(id)
|| ctx.integer_locals.contains(id)
|| ctx.unsigned_i32_locals.contains(id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Include declared numeric types in the update proof.

This early return bypasses is_numeric_expr, so updates to Number/Int32 locals not present in the integer sets conservatively miss the inline bitwise path (for example, numberLocal++ ^ "1"). Mirror the declared-type check from Lines 129-132 here.

Proposed fix
 Expr::Update { id, .. } => {
-    return ctx.not_bigint_locals.contains(id)
+    return matches!(
+        ctx.local_types.get(id),
+        Some(HirType::Number) | Some(HirType::Int32)
+    ) || ctx.not_bigint_locals.contains(id)
         || ctx.integer_locals.contains(id)
         || ctx.unsigned_i32_locals.contains(id);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Expr::Update { id, .. } => {
return ctx.not_bigint_locals.contains(id)
|| ctx.integer_locals.contains(id)
|| ctx.unsigned_i32_locals.contains(id);
Expr::Update { id, .. } => {
return matches!(
ctx.local_types.get(id),
Some(HirType::Number) | Some(HirType::Int32)
) || ctx.not_bigint_locals.contains(id)
|| ctx.integer_locals.contains(id)
|| ctx.unsigned_i32_locals.contains(id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/type_analysis/numeric.rs` around lines 392 - 395,
Update the Expr::Update branch in the numeric proof to also recognize locals
whose declared types are Number or Int32, mirroring the declared-type check used
by is_numeric_expr around lines 129-132. Preserve the existing
not_bigint_locals, integer_locals, and unsigned_i32_locals checks while ensuring
declared numeric locals qualify for the inline bitwise path.

@proggeramlug
proggeramlug merged commit c06c434 into main Jul 27, 2026
29 of 30 checks passed
@proggeramlug
proggeramlug deleted the perf/inline-nonbigint-bitwise branch July 27, 2026 02:44
proggeramlug pushed a commit that referenced this pull request Jul 27, 2026
…context

A typed-array element read through a parameter (`function f(S: Int32Array)`)
that feeds arithmetic rather than `| 0` — the bcryptjs `_encipher` S-box shape
`n = S[l >>> 24]; n += S[...]` — previously compiled to a per-read
`call js_typed_array_get(ptr, idx)`. The i32 fast path only fires in a ToInt32
(`| 0`) context, so these numeric-context reads stayed on the runtime call.

Lower them to an inline checked f64 load instead: the same guard as the checked
i32 read (pointer + inline-storage PERRY_TA_VIEW_GUARD==0 + kind-cache), a header
bounds check, a bare per-kind load widened to f64 (sitofp/uitofp/fpext/direct),
and — bit-exact with js_typed_array_get — the TAG_UNDEFINED double on OOB /
negative. Guard misses defer to a new memory-safe cold helper
js_typed_array_read_f64 (validates the pointer is a registered typed array
before any header deref, mirroring js_typed_array_read_int32). Gated on a proven
non-negative integer index (a fractional `S[3.9]` must read undefined, not
element 3). Covers every numeric kind incl. Uint32 (unsigned widening) and the
float kinds. Env flag PERRY_TA_PARAM_F64_READ (default on), keyed into the
object cache.

Measured on the real _encipher shape (2.1M calls, quiet M1, min-of-9,
byte-exact): 1787ms -> 1351ms, ~1.32x; the per-read guard costs only ~3% over a
bare load, so no guard hoisting is needed. Stacks with #6860 (inline non-BigInt
bitwise): the dynamic-bitwise baseline was ~3541ms.
proggeramlug pushed a commit that referenced this pull request Jul 27, 2026
…context

A typed-array element read through a parameter (`function f(S: Int32Array)`)
that feeds arithmetic rather than `| 0` — the bcryptjs `_encipher` S-box shape
`n = S[l >>> 24]; n += S[...]` — previously compiled to a per-read
`call js_typed_array_get(ptr, idx)`. The i32 fast path only fires in a ToInt32
(`| 0`) context, so these numeric-context reads stayed on the runtime call.

Lower them to an inline checked f64 load instead: the same guard as the checked
i32 read (pointer + inline-storage PERRY_TA_VIEW_GUARD==0 + kind-cache), a header
bounds check, a bare per-kind load widened to f64 (sitofp/uitofp/fpext/direct),
and — bit-exact with js_typed_array_get — the TAG_UNDEFINED double on OOB /
negative. Guard misses defer to a new memory-safe cold helper
js_typed_array_read_f64 (validates the pointer is a registered typed array
before any header deref, mirroring js_typed_array_read_int32). Gated on a proven
non-negative integer index (a fractional `S[3.9]` must read undefined, not
element 3). Covers every numeric kind incl. Uint32 (unsigned widening) and the
float kinds. Env flag PERRY_TA_PARAM_F64_READ (default on), keyed into the
object cache.

Measured on the real _encipher shape (2.1M calls, quiet M1, min-of-9,
byte-exact): 1787ms -> 1351ms, ~1.32x; the per-read guard costs only ~3% over a
bare load, so no guard hoisting is needed. Stacks with #6860 (inline non-BigInt
bitwise): the dynamic-bitwise baseline was ~3541ms.
proggeramlug added a commit that referenced this pull request Jul 27, 2026
…context (#6883)

* perf(codegen): inline checked-f64 typed-array-param reads in numeric context

A typed-array element read through a parameter (`function f(S: Int32Array)`)
that feeds arithmetic rather than `| 0` — the bcryptjs `_encipher` S-box shape
`n = S[l >>> 24]; n += S[...]` — previously compiled to a per-read
`call js_typed_array_get(ptr, idx)`. The i32 fast path only fires in a ToInt32
(`| 0`) context, so these numeric-context reads stayed on the runtime call.

Lower them to an inline checked f64 load instead: the same guard as the checked
i32 read (pointer + inline-storage PERRY_TA_VIEW_GUARD==0 + kind-cache), a header
bounds check, a bare per-kind load widened to f64 (sitofp/uitofp/fpext/direct),
and — bit-exact with js_typed_array_get — the TAG_UNDEFINED double on OOB /
negative. Guard misses defer to a new memory-safe cold helper
js_typed_array_read_f64 (validates the pointer is a registered typed array
before any header deref, mirroring js_typed_array_read_int32). Gated on a proven
non-negative integer index (a fractional `S[3.9]` must read undefined, not
element 3). Covers every numeric kind incl. Uint32 (unsigned widening) and the
float kinds. Env flag PERRY_TA_PARAM_F64_READ (default on), keyed into the
object cache.

Measured on the real _encipher shape (2.1M calls, quiet M1, min-of-9,
byte-exact): 1787ms -> 1351ms, ~1.32x; the per-read guard costs only ~3% over a
bare load, so no guard hoisting is needed. Stacks with #6860 (inline non-BigInt
bitwise): the dynamic-bitwise baseline was ~3541ms.

* docs(changelog): add fragment for #6883

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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.

1 participant