diff --git a/crates/common/trie/node/branch.rs b/crates/common/trie/node/branch.rs index 7a5df13d25e..69c2cdb08d5 100644 --- a/crates/common/trie/node/branch.rs +++ b/crates/common/trie/node/branch.rs @@ -281,7 +281,7 @@ impl BranchNode { /// Computes the node's hash, using the provided buffer pub fn compute_hash_no_alloc(&self, buf: &mut Vec, crypto: &dyn Crypto) -> NodeHash { buf.clear(); - self.encode(buf); + self.encode_into_vec(buf); let hash = NodeHash::from_encoded(buf, crypto); buf.clear(); hash diff --git a/crates/common/trie/rlp.rs b/crates/common/trie/rlp.rs index ddd4802598c..c224691a080 100644 --- a/crates/common/trie/rlp.rs +++ b/crates/common/trie/rlp.rs @@ -27,14 +27,19 @@ use crate::{Nibbles, NodeHash}; // where `NativeCrypto` is the correct provider. impl RLPEncode for BranchNode { fn encode(&self, buf: &mut dyn bytes::BufMut) { + // Resolve each child's hash once: the length pass and the encode pass + // both needed it, so a 16-choice branch was paying 32 resolutions. + let hashes: [&NodeHash; 16] = + array::from_fn(|i| self.choices[i].compute_hash_ref(&NativeCrypto)); + let value_len = <[u8] as RLPEncode>::length(&self.value); - let payload_len = self.choices.iter().fold(value_len, |acc, child| { - acc + RLPEncode::length(child.compute_hash_ref(&NativeCrypto)) - }); + let payload_len = hashes + .iter() + .fold(value_len, |acc, hash| acc + RLPEncode::length(*hash)); encode_length(payload_len, buf); - for child in self.choices.iter() { - match child.compute_hash_ref(&NativeCrypto) { + for hash in hashes { + match hash { NodeHash::Hashed(hash) => hash.0.encode(buf), NodeHash::Inline((_, 0)) => buf.put_u8(RLP_NULL), NodeHash::Inline((encoded, len)) => buf.put_slice(&encoded[..*len as usize]), @@ -69,6 +74,38 @@ impl RLPEncode for BranchNode { } } +impl BranchNode { + /// Concrete-typed sibling of `::encode`, appending into a + /// `Vec` rather than a `&mut dyn BufMut`. + /// + /// `RLPEncode::encode` must take a trait object, so hashing a branch paid a + /// vtable dispatch for each of its ~41 `put_u8`/`put_slice` calls. Hashing is + /// the hot consumer (every `memoize_hashes` walk re-encodes each dirty + /// branch), so it gets a monomorphic path; `RLPEncode::encode` is untouched + /// for every other caller. + pub fn encode_into_vec(&self, buf: &mut Vec) { + let hashes: [&NodeHash; 16] = + array::from_fn(|i| self.choices[i].compute_hash_ref(&NativeCrypto)); + + let value_len = <[u8] as RLPEncode>::length(&self.value); + let payload_len = hashes + .iter() + .fold(value_len, |acc, hash| acc + RLPEncode::length(*hash)); + + encode_length(payload_len, buf); + for hash in hashes { + match hash { + NodeHash::Hashed(hash) => hash.0.encode(&mut *buf), + NodeHash::Inline((_, 0)) => buf.push(RLP_NULL), + NodeHash::Inline((encoded, len)) => { + buf.extend_from_slice(&encoded[..*len as usize]) + } + } + } + <[u8] as RLPEncode>::encode(&self.value, buf); + } +} + impl RLPEncode for ExtensionNode { fn encode(&self, buf: &mut dyn bytes::BufMut) { let mut encoder = Encoder::new(buf).encode_bytes(&self.prefix.encode_compact());