Skip to content

perf(runtime,codegen): SSO computed keys + megamorphic write stub cache (dynamic-property overwrite 5.3×) - #8965

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf-write-stub-cache
Aug 28, 2026
Merged

perf(runtime,codegen): SSO computed keys + megamorphic write stub cache (dynamic-property overwrite 5.3×)#8965
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf-write-stub-cache

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Computed string keys (o["k" + i]) stopped defeating every property cache in the pipeline.

Dynamic-property overwrite loop: ~450 ms → ~86 ms (5.3×), 8/8 interleaved A/B pairs at stable load; node on the same host runs it in 28–30 ms, so this workload goes from ~16× node to ~3.0× node. A second 10/10 run under heavy co-tenant load (everything inflated ~1.8×) reproduced the same ratio. Binary size +4 KB (+0.04%) — runtime-side, no generated-code growth.

Two changes that only pay off together

1. js_string_concat_value_box — SSO result when it fits. The fused string+number concat now returns an inline SSO immediate for results of ≤ 5 ASCII bytes instead of always minting a StringHeader. That removes a heap allocation per iteration from the hot "prefix" + i shape, but the bigger effect is that the result's bits become content-stable, so caches that compare key values can hit. ASCII-only, for the same utf16_len soundness reason as js_string_concat_box's existing SSO arm.

2. A megamorphic stub cache for dynamic string-keyed writes. A site's inline IC holds DYN_IC_WAYS = 3; a loop rotating 500 computed keys through it evicts permanently and pays the full miss walk every write. Added a thread-local 4096-way direct-mapped cache keyed on (shape_token, key_bits) — V8's megamorphic stub pattern — probed after the per-site ways miss and fed at every prime site, including for overflow slots (a wide object keeps every data property there, so gating on the inline region would starve the cache for exactly the receivers it exists for).

Supporting: the SSO→heap materialization every *const StringHeader consumer crosses now interns instead of minting, so a computed key's address is stable too and address-keyed read plans can hit; short heap keys are folded to their SSO bits before keying.

Safety

The stub stores only content-derived bits, never an address; keys that don't fit the inline form are rejected rather than cached under their pointer. This is load-bearing, not conservatism: dyn_ic_try_store revalidates the receiver's current shape token, blocking flags and slot bound on every hit, but it confirms the shape, not that the cached slot belongs to this key. A pointer-keyed entry could be primed, evicted from the (direct-mapped, evict-on-collision) intern table, collected, and its address recycled by an unrelated string whose write would hit the stale entry and overwrite the wrong slot. Content-only keying removes that class entirely and leaves the table holding no GC roots (registered in gc_runtime_root_holders.json with that verdict).

Method note — two wrong verdicts before the right one

The stub alone measured as a wash, twice, and no profile explained why. Counters did, in two stages:

  • 600k inserts, 1.2M probes, 0 hits. Splitting the probe-miss counter by cause (empty / token / key) showed 99% key-mismatch: the keys were fresh heap pointers each iteration, so a value-keyed cache could never hit at any capacity.
  • After the SSO fix: still only 9,584 hits of 1.19M. The way index XOR'd the low bits, and an SSO key's low bits are its first byte"k0".."k499" collapsed onto 125 ways with buckets 10 deep, evicting each other continuously. Multiplicative mixing over the top bits gives 480/500 distinct ways, and that single change is what produced the 5.3×.

Verification

  • perry-runtime 2774 passed / 0 failed; perry-codegen 1340 passed / 0 failed.
  • scripts/run_lint_gates.sh: all gates pass (ci_cargo_test_shard.py needs --total-shards 8, which passes when supplied).
  • Differential vs node on computed-key behaviour — delete/re-add key ordering, Map/Set keys, the 5→6 byte SSO boundary, non-ASCII, floats, negatives, 1e21, numeric-looking keys: byte-identical output.
  • Two codegen IR tests updated to expect the _box twin; their intent (fused concat, no tag test) is unchanged.

benchmarks/bench_populated_delete.ts (the ~200× populated-delete gap) is unaffected by this path and remains open for dictionary mode.

https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP

Summary by CodeRabbit

  • Performance

    • Improved dynamic property writes using computed string keys, reducing repeated write overhead.
    • Short string concatenation can now avoid heap allocation in common cases.
    • Reused equivalent short-string values to improve cache effectiveness.
  • Diagnostics

    • Added an optional hardened allocator mode for investigating memory-related issues.

Computed string keys (`o["k" + i]`) stopped defeating every property cache
in the pipeline: the dynamic-property overwrite loop goes from ~450 ms to
~86 ms — 5.3x, 8/8 interleaved A/B pairs at stable load — against 28-30 ms
for node on the same host.

Two changes that only pay off together.

1. `js_string_concat_value_box`: the fused string+number concat now returns
   SSO inline when the result fits (<= 5 ASCII bytes) instead of always
   minting a StringHeader. That removes a heap allocation per iteration from
   the hot `"prefix" + i` shape, but the bigger effect is that the result's
   BITS become content-stable, so caches that compare key values can hit.

2. A thread-local 4096-way megamorphic stub cache keyed on
   (shape_token, key_bits) for dynamic string-keyed writes. A site's inline
   IC holds DYN_IC_WAYS = 3; a loop rotating 500 keys through it evicts
   permanently. The stub is probed after the per-site ways miss and fed at
   every prime site, including for overflow slots.

Supporting: SSO->heap materialization interns instead of minting, so a
computed key's ADDRESS is stable too (address-keyed read plans hit); short
heap keys are folded to their SSO bits before keying; the way index uses
multiplicative mixing (a low-bit XOR collapsed 500 string keys onto 125 ways,
worst bucket 10 deep, and was the single reason the cache measured as a wash).

Safety: the stub stores only content-derived bits — never an address — so no
entry can be invalidated by an intern eviction recycling a heap address, and
the table holds no GC roots. Stale entries are rejected by dyn_ic_try_store's
per-hit shape-token/flags/slot-bound revalidation.

Runtime-side; the binary grows 4 KB (+0.04%).

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

coderabbitai Bot commented Aug 28, 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: d41b6d16-8ac2-4b5d-aafd-d3f22e076817

📥 Commits

Reviewing files that changed from the base of the PR and between bdd3952 and 9c43af6.

📒 Files selected for processing (10)
  • changelog.d/8952-sso-computed-keys-write-stub.md
  • crates/perry-codegen/src/codegen/declared_string_add_tests.rs
  • crates/perry-codegen/src/lower_string_concat.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-runtime/Cargo.toml
  • crates/perry-runtime/src/proxy/put_value.rs
  • crates/perry-runtime/src/string/alloc.rs
  • crates/perry-runtime/src/string/concat.rs
  • crates/perry-runtime/src/string/mod.rs
  • scripts/gc_runtime_root_holders.json

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The computed-key write path now produces short ASCII keys as SSO immediates and caches dynamic writes by shape token and content-derived key bits. The cache validates entries and supports overflow slots. An optional hardened allocator feature and GC-root metadata were added.

Computed-key write optimization

Layer / File(s) Summary
NaN-boxed string concatenation
crates/perry-codegen/src/runtime_decls/strings.rs, crates/perry-codegen/src/lower_string_concat.rs, crates/perry-codegen/src/codegen/declared_string_add_tests.rs, crates/perry-runtime/src/string/concat.rs
The fused string-plus-value concat path now calls js_string_concat_value_box. Eligible short ASCII results use SSO immediates. Codegen assertions expect the new double-returning helper.
Canonical key representation
crates/perry-runtime/src/string/alloc.rs, crates/perry-runtime/src/string/mod.rs
SSO-to-heap materialization now interns short strings. Short ASCII heap strings can be converted to equivalent SSO key bits.
Megamorphic dynamic write cache
crates/perry-runtime/src/proxy/put_value.rs, scripts/gc_runtime_root_holders.json, crates/perry-runtime/Cargo.toml, changelog.d/8952-sso-computed-keys-write-stub.md
Dynamic writes use a thread-local 4096-way cache keyed by shape token and content-derived key bits. Cache hits are revalidated, overflow slots are supported, and the cache is recorded as non-GC data. The changelog and diagnostic allocator feature document the implementation and benchmark.

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

Merge Risk: ⚪ Minimal · up to 9c43a

This PR optimizes short computed-key writes without changing their validated behavior; the supplied test and differential results show no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant GeneratedCode
  participant js_string_concat_value_box
  participant js_put_value_set_dyn_ic
  participant WRITE_STUB
  participant dyn_ic_try_store
  GeneratedCode->>js_string_concat_value_box: build computed property key
  GeneratedCode->>js_put_value_set_dyn_ic: perform dynamic property write
  js_put_value_set_dyn_ic->>WRITE_STUB: probe shape token and key bits
  WRITE_STUB->>dyn_ic_try_store: validate cached slot
  dyn_ic_try_store-->>GeneratedCode: return stored value
Loading

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides detailed technical context, performance results, safety rationale, and verification results. However, it does not use the required template sections or provide the required te… Restructure the description using the repository template. Add Summary, Changes, Related issue, Test plan, Screenshots / output, and Checklist sections. Include the executed verification commands and mark the applicable checklist items. Set…
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies both primary performance changes: SSO computed keys and the megamorphic write stub cache. It also states the measured overwrite improvement.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 7 files. (3 skipped: 3…
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.
Full details: Description check

Explanation

The description provides detailed technical context, performance results, safety rationale, and verification results. However, it does not use the required template sections or provide the required test-plan and checklist entries.

Resolution

Restructure the description using the repository template. Add Summary, Changes, Related issue, Test plan, Screenshots / output, and Checklist sections. Include the executed verification commands and mark the applicable checklist items. Set Related issue to an issue reference or "n/a" if standalone. Do not include the Claude session URL as a substitute for the required sections.

Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 7 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged (batched with #8966). The two changes do depend on each other — an SSO result from the fused concat is what lets the megamorphic write stub cache hit at all — and both are additive fast paths with the existing routes as the fallback.

Validation: codegen 1341/0, runtime 2779/0 (RUST_TEST_THREADS=1); evac failing set 16, the pre-existing one; scripts/run_lint_gates.sh 57 of 58 with the compile tier green — the exception is the pre-existing \${{ }} artifact (#8929).

The 5.3× is not re-measured here.

@proggeramlug
proggeramlug merged commit 18a28af into PerryTS:main Aug 28, 2026
20 checks passed
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