Skip to content

perf(codegen/runtime): #6812 — key-table clone lane: w12 beats node 6× - #6902

Merged
proggeramlug merged 4 commits into
mainfrom
perf/6812-keytable-lane
Jul 27, 2026
Merged

perf(codegen/runtime): #6812 — key-table clone lane: w12 beats node 6×#6902
proggeramlug merged 4 commits into
mainfrom
perf/6812-keytable-lane

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

w12 falls: 3 ms vs node ~18 ms — 6× faster than node (was 82 ms before this campaign arc; 58 ms after the dynamic-key IC in #6895). The scoreboard reaches 15 of 18 matrix rows beating node.

The slice teaches the whole-loop clone the table-driven write o[K[idx]] = v where K is a loop-invariant local holding an array of strings:

  • Grammar: % joins the numeric matcher with an exact integer range ([0, c-1] for a proven-nonnegative dividend and constant positive divisor); a new integer-valuedness proof gates index expressions so fptosi/truncation can never change semantics.
  • Matcher (v1 constraints): single group, sole store, constant bounds, index range hi ≤ 3 (the shared guard's lane width). The table local passes the same invariance checks as the receiver array.
  • Preflight wrapper (js_object_array_keytable_write_guard): validates the key table (live array, length covers the proven range, entries are strings), materializes each entry to a stable heap string — array/table/keys rooted across SSO materialization — and reuses object_array_numeric_write_slots verbatim for the receiver-prefix proof, spill lanes included, writing resolved lanes into a caller-provided stack table.
  • Nest: the index expression is emitted in the integer domain from the counters' native i32 registers (i64 add/sub/mul/sremfrem lowers to an fmod library call on AArch64, which was the entire 25 → 3 ms residual), indexes the slot table (range-proven, no bounds check), and stores through the same inline/spill shapes as compile-time lanes.

Validation

  • dynkey_ic_sanity.ts grew to 13 cases: the key-table adversarials cover a SHORT table (guard rejects; fallback preserves the exact TypeError timing node produces) and a non-string table entry (guard rejects; generic path byte-identical) — all byte-identical vs node v26.3.0, including under PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1.
  • All existing sanity batteries byte-identical; suites (serial) perry-hir 217 / perry-codegen 254 / perry-runtime 1467 green; gap gate OK (only the 7 long-standing triaged entries).
  • Idle-rig medians (15-run protocol on the reference mini): w12 3 ms vs node 18 ms, checksums identical; guard trace shows zero rejections.

Refs #6812.

https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG

Summary by CodeRabbit

  • Performance Improvements

    • Improved performance for dense object-array writes that use dynamically selected, rotating keys, including new support for a table-driven key write pattern.
    • Reduced overhead in supported numeric/object write loops by optimizing how keys are validated and how per-key destination slots are resolved.
    • Integer key calculations are now handled more efficiently, and reported benchmarks show a substantial improvement (e.g., rotating-key writes up to ~4.7×).
  • Documentation

    • Updated the object-write matrix triage notes to reflect the revised “beats the node” outcome for the affected scenario.

Ralph Küpper added 3 commits July 27, 2026 10:32
… clone tier)

The matcher recognizes a single-group body whose sole store's key is
K[idx] for a loop-invariant local K and an integer-valued, range-proven
index expression (Mod joins the numeric grammar with a [0, c-1] range for
nonnegative dividends and constant positive divisors). The preflight
wrapper guard validates K (live array, length covers the proven range,
entries are strings), materializes each entry to a stable heap string
(rooted across SSO materialization), and reuses
object_array_numeric_write_slots verbatim for the receiver-prefix proof —
spill lanes included — writing resolved lanes into a stack table. The
nest indexes the table with the range-proven expression (no bounds check
needed) and stores through the same inline/spill shape as compile-time
lanes. Rotating-key writes over a constant key array now run call-free.

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG
…e lane

frem lowers to an fmod library call on AArch64 (~10ns per element). Index
expressions are integer-proven, so emit them from the counters' native
i32 registers as i64 add/sub/mul/srem — srem equals JS % on the proven
nonnegative-dividend positive-divisor domain — and index the slot table
directly, no float round-trip.

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 99947a5c-3c0b-416a-9c34-fbd6d983ecbd

📥 Commits

Reviewing files that changed from the base of the PR and between 3ebbd26 and 514c72c.

📒 Files selected for processing (1)
  • crates/perry-codegen/src/stmt/loops.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-codegen/src/stmt/loops.rs

📝 Walkthrough

Walkthrough

Adds a table-driven fast lane for dense object-array writes, supporting invariant string key tables and integer index expressions including modulo. Runtime preflight resolves table keys to slots, and codegen emits resolved inline or spill stores.

Changes

Key-table object-array write lane

Layer / File(s) Summary
Match and prove table-driven stores
crates/perry-codegen/src/stmt/loops.rs
The matcher recognizes o[K[idx]] = v, supports integer-valued % expressions, proves finite index ranges, and enforces the table lane as the sole store lane.
Resolve key-table slots
crates/perry-runtime/src/proxy/put_value.rs
The runtime guard validates the key table, materializes heap-string keys, resolves numeric receiver slots, and encodes inline/spill metadata.
Emit key-table fast stores
crates/perry-codegen/src/stmt/loops.rs, changelog.d/6812-keytable-lane.md, docs/object-write-matrix.md
Codegen invokes the guard, bypasses classic lane decoding, computes i64 indices, and emits resolved inline or spill stores; changelog and matrix entries document the lane and benchmark result.

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

Sequence Diagram(s)

sequenceDiagram
  participant ObjectArrayWriteMatcher
  participant lower_object_array_write_versioned_for
  participant js_object_array_keytable_write_guard
  participant FastLoop
  ObjectArrayWriteMatcher->>lower_object_array_write_versioned_for: match o[K[idx]] = v
  lower_object_array_write_versioned_for->>js_object_array_keytable_write_guard: validate table and resolve slots
  js_object_array_keytable_write_guard-->>lower_object_array_write_versioned_for: return encoded slot lanes
  lower_object_array_write_versioned_for->>FastLoop: compute i64 index and emit slot-based store
Loading

Possibly related PRs

  • PerryTS/perry#6821: Introduces the generalized receiver-prefix numeric write proof reused by the key-table guard.
  • PerryTS/perry#6822: Adds related object-array numeric slot-validation infrastructure reused by this lane.
  • PerryTS/perry#6856: Updates inline-versus-spill slot classification reused by the key-table guard.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the key-table clone lane performance change and its main benchmark outcome.
Description check ✅ Passed The description includes a strong summary, concrete validation, and the related issue, though some template sections are missing or abbreviated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/6812-keytable-lane

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 (3)
crates/perry-codegen/src/stmt/loops.rs (3)

3054-3063: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the dead lane binding.

lane is only consumed by let _ = lane;; return the two derived registers directly.

♻️ Proposed cleanup
-        let (lane, spill_flag, slot) = {
+        let (spill_flag, slot) = {
             let blk = ctx.block();
             let lane_ptr = blk.gep(I64, out_alloca, &[(I64, &idx_i64)]);
             let lane = blk.load(I64, &lane_ptr);
             let spill_flag = blk.and(I64, &lane, "32768");
             let low = blk.and(I64, &lane, "32767");
             let slot = blk.sub(I64, &low, "1");
-            (lane, spill_flag, slot)
+            (spill_flag, slot)
         };
-        let _ = lane;
🤖 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/stmt/loops.rs` around lines 3054 - 3063, Remove the
unused lane binding and the trailing let _ = lane statement in the block
computing spill_flag and slot. Return only spill_flag and slot from the block,
and update the destructuring binding accordingly while preserving their existing
calculations.

3107-3117: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Table mode still emits the group-0 element load per iteration.

In table mode slots is empty (decode_lanes == 0) but group_plans still contains the primary group, so this block emits a dead element load/bitcast/inttoptr chain each inner iteration before the zip produces zero lanes. It is DCE-able and correct, but skipping the primary plan when key_table.is_some() makes the invariant explicit rather than relying on the length-mismatched zip.

🤖 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/stmt/loops.rs` around lines 3107 - 3117, Skip the
primary group plan in table mode before the per-iteration object pointer
construction in the group_plans loop. Use key_table.is_some() to exclude group 0
while retaining all group plans for non-table mode, and preserve the existing
zero-lane behavior without emitting the unused load/bitcast/inttoptr chain.

2451-2533: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider flattening this recognition path.

Seven levels of nested if/if let make the accept/reject conditions hard to audit. Extracting the table-lane recognition into a helper returning Option<KeyTableLane> (with ?-style early exits) would keep the reject-vs-continue distinction explicit — note the subtle contract that a matched IndexGet key always terminates the store loop (t2.is_empty()) or returns None.

🤖 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/stmt/loops.rs` around lines 2451 - 2533, Refactor
the table-lane recognition inside the loop’s key handling into a dedicated
helper returning Option<KeyTableLane>, using early exits for the existing
eligibility checks and index/value validation. Preserve the contract that any
matched IndexGet key terminates processing: return None for rejected candidates,
while only accepted lanes update key_table, values, cursor, and continue; retain
the surrounding key_table rejection behavior.
🤖 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/stmt/loops.rs`:
- Around line 2612-2649: Reject integer-valued numeric constants outside the
exact i64 range in object_array_write_number_integer_valued before
emit_object_array_write_index_i64 is reached. Preserve the existing
finite-and-integral checks, add an exact representability check for the
constant, and ensure out-of-range values do not take the i64 emission path.

---

Nitpick comments:
In `@crates/perry-codegen/src/stmt/loops.rs`:
- Around line 3054-3063: Remove the unused lane binding and the trailing let _ =
lane statement in the block computing spill_flag and slot. Return only
spill_flag and slot from the block, and update the destructuring binding
accordingly while preserving their existing calculations.
- Around line 3107-3117: Skip the primary group plan in table mode before the
per-iteration object pointer construction in the group_plans loop. Use
key_table.is_some() to exclude group 0 while retaining all group plans for
non-table mode, and preserve the existing zero-lane behavior without emitting
the unused load/bitcast/inttoptr chain.
- Around line 2451-2533: Refactor the table-lane recognition inside the loop’s
key handling into a dedicated helper returning Option<KeyTableLane>, using early
exits for the existing eligibility checks and index/value validation. Preserve
the contract that any matched IndexGet key terminates processing: return None
for rejected candidates, while only accepted lanes update key_table, values,
cursor, and continue; retain the surrounding key_table rejection behavior.
🪄 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: b146ae3c-8043-49d5-9af2-28dadbe22615

📥 Commits

Reviewing files that changed from the base of the PR and between d27948f and 3ebbd26.

📒 Files selected for processing (4)
  • changelog.d/6812-keytable-lane.md
  • crates/perry-codegen/src/stmt/loops.rs
  • crates/perry-runtime/src/proxy/put_value.rs
  • docs/object-write-matrix.md

Comment thread crates/perry-codegen/src/stmt/loops.rs
An integral-but-huge double (1e19) saturates in the as-i64 lowering, so
1e19 %% 4 would srem to 3 where JS evaluates 0. The integer-valuedness
proof now requires the round-trip (c as i64) as f64 == c; regression in
dynkey sanity s14 ((1e19 + i) %% 3 through the fallback, byte-identical).

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG
@proggeramlug
proggeramlug merged commit c9d961f into main Jul 27, 2026
3 checks passed
@proggeramlug
proggeramlug deleted the perf/6812-keytable-lane branch July 27, 2026 12:54
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