perf(mpt): compare node reference digests as words#677
Open
Qumeric wants to merge 1 commit into
Open
Conversation
The digest comparison during decoding ran a byte at a time, five instructions per byte, because a slice comparison of unknown length becomes a `memcmp` call. Fixing the length to 32 at the three sites where one side is a full digest lets it compile to four word loads per side and an XOR chain instead. Shorter references keep the byte loop, which is still the cheaper form for them.
Qumeric
marked this pull request as ready for review
July 25, 2026 11:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Decoding compares every node's reference against the one its parent stated.
bytes_eqwalks thosebyte by byte, which costs about five instructions per byte: two loads, two pointer bumps and a
branch. At 32 bytes that is ~160 instructions per comparison, and the decoder performs one per node.
The comment on
bytes_eqexplains why it was written that way — slice==becomes amemcmpcall— and that is still true for slices whose length the compiler cannot see. It is not true when the
length is fixed: the guest target enables
+unaligned-scalar-mem, so a 32-byte comparison compilesto eight word loads and an XOR chain, 17 straight-line instructions with no call and no loop.
Three of the four call sites compare a full digest, so they go through a
digest_eqhelper thatfixes the length. The fourth compares a reference shorter than 32 bytes and keeps the byte loop,
which is still the cheaper form there.
digest_eqis correct for any input because slices ofdifferent lengths are never equal, so a reference that is not a full digest is simply inequality.
execute_metered_insns616,699,072 -> 587,583,276, -4.721% (block 24001988, execute-metered).Rows -4.577%, main cells -1.601%, interaction cells -2.883%, metered memory -0.836%.
The new test covers the direction that matters for soundness: a corrupted digest must be rejected,
including when only its last byte differs, which is what a comparison that stopped short would miss.
The length is fixed here because
NodeRef::Digestis documented as always 32 bytes, but the variantholds a
&[u8]and the invariant is only enforced by adebug_assertthat release builds drop.Changing it to
&[u8; 32]would make this helper unnecessary and enforce the invariant in the type;left as a follow-up so this change stays measurable on its own.