Skip to content

perf(string): fuse accumulator concat chains - #8683

Merged
proggeramlug merged 3 commits into
mainfrom
merge/b9
Aug 24, 2026
Merged

perf(string): fuse accumulator concat chains#8683
proggeramlug merged 3 commits into
mainfrom
merge/b9

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Lands #8497 (refs #8410) — fuses s = s + a + b + ... accumulator chains into a single rooted runtime call.

Audit

This touches the two things that are non-negotiable in this area — in-place string mutation and raw payload pointers held across an allocation — so I read both paths rather than trusting the suites.

Uniqueness gate. In-place reuse is gated on (*dest).refcount == 1 && total_blen <= (*dest).capacity. Fresh headers are created shared at refcount 0, so == 1 correctly admits only uniquely-owned accumulators, and the capacity bound is checked before the copy. The aliasing/overlap case has its own fallback test (string_append_chain_falls_back_for_overlap_and_dynamic_parts).

Rooting, runtime side. The fast path uses string_storage_alloc_no_collect, so the raw piece_ptrs cannot go stale there. On the collecting fallback it roots every piece in a RuntimeHandleScope before string_storage_alloc, and then reloads each one through with_const_ptr rather than reusing the pre-collection raw pointer:

handles[i].expect("append-chain string handle")
    .with_const_ptr::<StringHeader, _>(|piece| {
        ptr::copy_nonoverlapping(string_data(piece), cursor, len);
    });

That is the #7341 API used as intended — rooting and reloading, not rooting alone.

Rooting, codegen side. with_rooted_group adopts the accumulator and each suffix operand, then re-reads every one via reread_emitted/reread after all lowering that can collect. Reordering without the re-read is exactly what left a stale argument in #8427; this does the re-read.

js_string_append_chain also matches its pre-existing sibling js_string_concat_chain exactly in signature and extern "C" convention, so it introduces no new ABI surface.

Validation

  • 9 ratchet gates + cargo fmt --all --check: pass (including string_payload_access_inventory.py, the project's own borrow instrument)
  • perry-codegen --lib: 1198 passed, 0 failed
  • perry-runtime --lib (RUST_TEST_THREADS=1): 2654 passed, 0 failed
  • All three new tests verified running by name, not inferred from the total
  • Pushed tree verified identical to the validated tree

No version-file changes; the author's changelog.d/ fragment carried through.

The author's own A/B used the correct discipline — identical -p perry -p perry-runtime-static -p perry-stdlib-static set on both arms with archive mtimes verified after each build, five shuffled interleaved repeats, medians reported.

Summary by CodeRabbit

  • Performance

    • Improved chained string concatenation, reducing unnecessary intermediate allocations.
    • Benchmarks show up to 9.34% fewer CPU cycles in affected workloads, with minimal impact elsewhere.
  • Bug Fixes

    • Improved handling of self-appending strings, including capacity reuse and dynamic values.
    • Preserved correct string lengths, encoding, ownership, and fallback behavior across chained appends.
  • Tests

    • Added coverage for chained appends, buffer reuse, ownership behavior, overlapping values, and non-string inputs.

Ralph Küpper added 3 commits August 21, 2026 01:15
Lands #8497 (refs #8410).

Recognizes `s = s + a + b + ...` for proven string accumulators in local,
captured, and module-global slots, and lowers the accumulator plus its
suffix operands into a single rooted `js_string_append_chain` call that
either reuses a uniquely-owned accumulator's spare capacity in place or
allocates the complete result exactly once. The existing dynamic-add
fallback is preserved wherever the string proof is incomplete.

Rooting reviewed rather than assumed. Codegen wraps the operands in
`with_rooted_group`, adopting the accumulator and each suffix part, then
re-reads every one through the group after all lowering that can collect
-- reordering alone would leave a stale argument (#8427). The runtime
takes the non-collecting `string_storage_alloc_no_collect` path first, so
the raw piece pointers cannot go stale there; on the collecting fallback
it roots every piece in a `RuntimeHandleScope` before
`string_storage_alloc` and then reloads each through
`with_const_ptr` instead of reusing the pre-collection raw pointer.

In-place reuse is gated on `refcount == 1 && total_blen <= capacity`,
which excludes shared (refcount 0) headers, and the aliasing/overlap case
has its own fallback test.
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8274e3d8-758a-4de3-a86d-caa80aa9c00a

📥 Commits

Reviewing files that changed from the base of the PR and between 67f3f75 and 0af737c.

📒 Files selected for processing (8)
  • changelog.d/8497-string-append-chain.md
  • crates/perry-codegen/src/codegen/declared_string_add_tests.rs
  • crates/perry-codegen/src/expr/literals_vars.rs
  • crates/perry-codegen/src/lower_string_concat.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-runtime/src/string/concat.rs
  • crates/perry-runtime/src/string/mod.rs
  • crates/perry-runtime/src/string/tests.rs

📝 Walkthrough

Walkthrough

Changes

String self-append chains now lower to one js_string_append_chain call. The runtime reuses unique accumulator capacity when possible and otherwise allocates the complete result once. Tests cover lowering, ownership, overlap, fallback conversion, and capacity reuse.

String append chain fusion

Layer / File(s) Summary
Runtime append-chain operation
crates/perry-runtime/src/string/concat.rs, crates/perry-runtime/src/string/mod.rs, crates/perry-runtime/src/string/tests.rs, changelog.d/8497-string-append-chain.md
The runtime adds heap-string append-chain handling, in-place capacity reuse, allocation fallback, string metadata updates, public re-export, and coverage for ownership, overlap, and dynamic values.
Code generation integration
crates/perry-codegen/src/runtime_decls/strings.rs, crates/perry-codegen/src/lower_string_concat.rs, crates/perry-codegen/src/expr/literals_vars.rs, crates/perry-codegen/src/codegen/declared_string_add_tests.rs
The code generator declares and emits js_string_append_chain for chained self-appends. Codegen tests now require the fused helper and reject the previous intermediate concat path.

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

Suggested reviewers: thehypnoo

Sequence Diagram(s)

sequenceDiagram
  participant Codegen
  participant js_string_append_chain
  participant StringStorage
  Codegen->>js_string_append_chain: pass rooted accumulator and suffix parts
  js_string_append_chain->>StringStorage: reuse capacity or allocate complete result
  StringStorage-->>js_string_append_chain: resulting string
  js_string_append_chain-->>Codegen: return updated string handle
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch merge/b9

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.

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