perf(l1): give the RLP encoder's scratch buffer an initial capacity - #7161
perf(l1): give the RLP encoder's scratch buffer an initial capacity#7161diegokingston wants to merge 1 commit into
Conversation
`Encoder` stages a struct's fields in `temp_buf` so the list header can be written once the payload length is known. That buffer started empty, so every encode paid a chain of doubling reallocations: an RLP list of a few hundred bytes grows 8 -> 16 -> ... -> 512, which is seven allocations and six copies to encode one node. Start it at 512 instead. That covers a trie node — the hashing path already sizes its own buffers at 512 as the maximum encoded node size — and the common transaction and receipt shapes, which is where the encoder is hottest. Larger payloads still grow from there; the point is to skip the small doublings, not to bound the size. The `PERF` note asking for exactly this is now addressed, so it goes away. Encoding is unchanged: capacity does not affect output, only how many times the buffer is reallocated on the way there. Measured on the LambdaVM zkVM guest, which runs ethrex's block execution as a RISC-V program, so every allocation is executed work rather than something the allocator can hide: block before after delta empty 609,404 586,983 -3.68% 1 transfer 1,097,816 1,059,997 -3.44% 10 transfers 3,702,726 3,604,757 -2.65% A guest profile attributes the win: `Encoder::encode_bytes`, which was 7-10% of all allocator cycles, disappears from the profile entirely, and total allocator cycles fall 23%.
|
🤖 Kimi Code ReviewThis is a well-crafted performance optimization. The change correctly addresses the reallocation overhead in RLP encoding without introducing any correctness or safety issues. Highlights:
Minor suggestion: Consider whether Verdict: LGTM. The 512-byte pre-allocation is a sensible trade-off given Ethereum's data structure sizes, and the comprehensive comment ensures future maintainers understand the rationale. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
I did not find correctness, consensus, gas-accounting, or security issues in the diff itself. The concern here is that the optimization is too global for a utility used pervasively in small nested encodes. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewGood — confirms the PR touches only Review: PR 7161 — RLP encoder scratch buffer initial capacityCorrectnessThe change is behavior-preserving: Performance — the main point worth scrutinizing
For these, the old The measurements in the PR description are all from block-execution workloads (empty/1-transfer/10-transfer blocks) in a zkVM guest, which is dominated by trie-node, transaction, and receipt encoding — exactly the cases 512 was tuned for. That's a solid result, but it doesn't tell us what this does to:
This may still be a net win overall (a single allocation call might cost roughly the same regardless of size on the profiled allocator, which would explain the results), but that's an assumption the PR doesn't validate for the small-payload call sites. Worth either:
DocumentationThe new doc comment on Minor
Automated review by Claude (Anthropic) · sonnet · custom prompt |
Lines of code reportTotal lines added: Detailed view |
|
Confirmed on a real block. Running the same guest against mainnet block 25368371 (45M cycles, where signature recovery is only 18.9% of the total rather than the ~65% it is on the synthetic transfer fixtures):
Consistent with the −2.65% measured on the 10-transfer fixture, so the win holds at real block size and isn't an artifact of the small fixtures. |
Motivation
Encoderstages a struct's fields intemp_bufso the list header can be written once the payload length is known. That buffer starts empty, so every encode pays a chain of doubling reallocations: an RLP list of a few hundred bytes grows 8 → 16 → … → 512, which is seven allocations and six copies to encode one node.The
PERFnote already inEncoder::newasks for exactly this.Description
Start
temp_bufat 512 bytes. That covers a trie node — the hashing path already sizes its own buffers at 512 as the maximum encoded node size — and the common transaction and receipt shapes, which is where the encoder is hottest. Larger payloads still grow from there; the point is to skip the small doublings, not to bound the size.Encoding output is unchanged: capacity only affects how many times the buffer is reallocated on the way there.
Measurements
Taken on the LambdaVM zkVM guest, which runs ethrex's block execution as a RISC-V program, so every allocation is executed work rather than something a host allocator can hide:
A guest profile attributes it:
Encoder::encode_bytes, previously 7–10% of all allocator cycles, disappears from the profile entirely, and total allocator cycles fall 23%.The capacity was picked by sweeping it — 64 gave −1.0%/−1.8%, 128 −1.7%/−2.7%, 512 −2.3%/−3.0%, 1024 −2.3%/−3.4%. Returns flatten after 512, which is also the bound the trie code already assumes.
Validation
cargo test -p ethrex-rlpandcargo test -p ethrex-common(171 tests, which exercise RLP round-trips) pass;cargo clippyclean.