mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-09 07:17:56 -05:00
perf(trie): add cursor locality optimization for storage cursors
Implement locality optimization for DatabaseStorageTrieCursor and DatabaseHashedStorageCursor that tracks the last key returned by the cursor. When seeking forward from the current position, the cursor now uses next_dup/next_dup_val (O(1)) to walk forward instead of performing expensive seek_by_key_subkey operations (O(log N)). This optimization targets the hot path identified in profiling data where StorageTrie showed 100% seeks (729.5K ops) and HashedStorages showed 76% seeks (2.5M ops) during state root calculation. The optimization: - Tracks last_key in both cursor types - When seek target > last_key, walks forward using next_dup - When seek target == last_key, returns cached current position - Falls back to seek_by_key_subkey for backward seeks or unpositioned cursor - Resets last_key when switching storage address
This commit is contained in:
@@ -78,18 +78,25 @@ where
|
||||
/// The structure wrapping a database cursor for hashed storage and
|
||||
/// a target hashed address. Implements [`HashedCursor`] and [`HashedStorageCursor`]
|
||||
/// for iterating over hashed storage.
|
||||
///
|
||||
/// This cursor implements locality optimization: when seeking forward from the current position,
|
||||
/// it uses `next_dup_val` (O(1)) to walk forward instead of `seek_by_key_subkey` (O(log N)) when
|
||||
/// the target key is close to the current position.
|
||||
#[derive(Debug)]
|
||||
pub struct DatabaseHashedStorageCursor<C> {
|
||||
/// Database hashed storage cursor.
|
||||
cursor: C,
|
||||
/// Target hashed address of the account that the storage belongs to.
|
||||
hashed_address: B256,
|
||||
/// The last key returned by the cursor, used for locality optimization.
|
||||
/// When seeking forward from this position, we can use `next_dup_val` instead of re-seeking.
|
||||
last_key: Option<B256>,
|
||||
}
|
||||
|
||||
impl<C> DatabaseHashedStorageCursor<C> {
|
||||
/// Create new [`DatabaseHashedStorageCursor`].
|
||||
pub const fn new(cursor: C, hashed_address: B256) -> Self {
|
||||
Self { cursor, hashed_address }
|
||||
Self { cursor, hashed_address, last_key: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,16 +106,50 @@ where
|
||||
{
|
||||
type Value = U256;
|
||||
|
||||
/// Seeks the given key in the hashed storage.
|
||||
///
|
||||
/// This method implements a locality optimization: if the cursor is already positioned
|
||||
/// and the target key is >= the current position, we use `next_dup_val` to walk forward
|
||||
/// instead of performing an expensive `seek_by_key_subkey` operation.
|
||||
fn seek(&mut self, subkey: B256) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
|
||||
Ok(self.cursor.seek_by_key_subkey(self.hashed_address, subkey)?.map(|e| (e.key, e.value)))
|
||||
// Locality optimization: if cursor is positioned and target is ahead,
|
||||
// walk forward using next_dup_val instead of seeking
|
||||
if let Some(last) = self.last_key {
|
||||
if subkey > last {
|
||||
// Walk forward using next_dup_val until we find a key >= target
|
||||
while let Some(entry) = self.cursor.next_dup_val()? {
|
||||
if entry.key >= subkey {
|
||||
self.last_key = Some(entry.key);
|
||||
return Ok(Some((entry.key, entry.value)));
|
||||
}
|
||||
}
|
||||
// Exhausted the duplicates, no match found
|
||||
self.last_key = None;
|
||||
return Ok(None);
|
||||
} else if subkey == last &&
|
||||
let Some((_, entry)) = self.cursor.current()? &&
|
||||
entry.key == subkey
|
||||
{
|
||||
// Re-seeking the same key, return current position if still valid
|
||||
return Ok(Some((entry.key, entry.value)));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to seek_by_key_subkey for backward seeks or when cursor is not positioned
|
||||
let result =
|
||||
self.cursor.seek_by_key_subkey(self.hashed_address, subkey)?.map(|e| (e.key, e.value));
|
||||
self.last_key = result.as_ref().map(|(k, _)| *k);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
|
||||
Ok(self.cursor.next_dup_val()?.map(|e| (e.key, e.value)))
|
||||
let result = self.cursor.next_dup_val()?.map(|e| (e.key, e.value));
|
||||
self.last_key = result.as_ref().map(|(k, _)| *k);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
// Database cursors are stateless, no reset needed
|
||||
self.last_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,5 +163,7 @@ where
|
||||
|
||||
fn set_hashed_address(&mut self, hashed_address: B256) {
|
||||
self.hashed_address = hashed_address;
|
||||
// Reset cursor position tracking when switching to a different storage
|
||||
self.last_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,18 +98,25 @@ where
|
||||
}
|
||||
|
||||
/// A cursor over the storage tries stored in the database.
|
||||
///
|
||||
/// This cursor implements locality optimization: when seeking forward from the current position,
|
||||
/// it uses `next_dup` (O(1)) to walk forward instead of `seek_by_key_subkey` (O(log N)) when
|
||||
/// the target key is close to the current position.
|
||||
#[derive(Debug)]
|
||||
pub struct DatabaseStorageTrieCursor<C> {
|
||||
/// The underlying cursor.
|
||||
pub cursor: C,
|
||||
/// Hashed address used for cursor positioning.
|
||||
hashed_address: B256,
|
||||
/// The last key returned by the cursor, used for locality optimization.
|
||||
/// When seeking forward from this position, we can use `next_dup` instead of re-seeking.
|
||||
last_key: Option<Nibbles>,
|
||||
}
|
||||
|
||||
impl<C> DatabaseStorageTrieCursor<C> {
|
||||
/// Create a new storage trie cursor.
|
||||
pub const fn new(cursor: C, hashed_address: B256) -> Self {
|
||||
Self { cursor, hashed_address }
|
||||
Self { cursor, hashed_address, last_key: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,27 +174,63 @@ where
|
||||
&mut self,
|
||||
key: Nibbles,
|
||||
) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
Ok(self
|
||||
let result = self
|
||||
.cursor
|
||||
.seek_by_key_subkey(self.hashed_address, StoredNibblesSubKey(key))?
|
||||
.filter(|e| e.nibbles == StoredNibblesSubKey(key))
|
||||
.map(|value| (value.nibbles.0, value.node)))
|
||||
.map(|value| (value.nibbles.0, value.node));
|
||||
|
||||
self.last_key = result.as_ref().map(|(k, _)| *k);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Seeks the given key in the storage trie.
|
||||
///
|
||||
/// This method implements a locality optimization: if the cursor is already positioned
|
||||
/// and the target key is >= the current position, we use `next_dup` to walk forward
|
||||
/// instead of performing an expensive `seek_by_key_subkey` operation.
|
||||
fn seek(
|
||||
&mut self,
|
||||
key: Nibbles,
|
||||
) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
Ok(self
|
||||
// Locality optimization: if cursor is positioned and target is ahead,
|
||||
// walk forward using next_dup instead of seeking
|
||||
if let Some(last) = self.last_key {
|
||||
if key > last {
|
||||
// Walk forward using next_dup until we find a key >= target
|
||||
while let Some((_, entry)) = self.cursor.next_dup()? {
|
||||
if entry.nibbles.0 >= key {
|
||||
self.last_key = Some(entry.nibbles.0);
|
||||
return Ok(Some((entry.nibbles.0, entry.node)));
|
||||
}
|
||||
}
|
||||
// Exhausted the duplicates, no match found
|
||||
self.last_key = None;
|
||||
return Ok(None);
|
||||
} else if key == last &&
|
||||
let Some((_, entry)) = self.cursor.current()? &&
|
||||
entry.nibbles.0 == key
|
||||
{
|
||||
// Re-seeking the same key, return current position if still valid
|
||||
return Ok(Some((entry.nibbles.0, entry.node)));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to seek_by_key_subkey for backward seeks or when cursor is not positioned
|
||||
let result = self
|
||||
.cursor
|
||||
.seek_by_key_subkey(self.hashed_address, StoredNibblesSubKey(key))?
|
||||
.map(|value| (value.nibbles.0, value.node)))
|
||||
.map(|value| (value.nibbles.0, value.node));
|
||||
|
||||
self.last_key = result.as_ref().map(|(k, _)| *k);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Move the cursor to the next entry and return it.
|
||||
fn next(&mut self) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
Ok(self.cursor.next_dup()?.map(|(_, v)| (v.nibbles.0, v.node)))
|
||||
let result = self.cursor.next_dup()?.map(|(_, v)| (v.nibbles.0, v.node));
|
||||
self.last_key = result.as_ref().map(|(k, _)| *k);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Retrieves the current value in the storage trie cursor.
|
||||
@@ -196,7 +239,7 @@ where
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
// No-op for database cursors
|
||||
self.last_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +249,8 @@ where
|
||||
{
|
||||
fn set_hashed_address(&mut self, hashed_address: B256) {
|
||||
self.hashed_address = hashed_address;
|
||||
// Reset cursor position tracking when switching to a different storage trie
|
||||
self.last_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user