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:
yongkangc
2026-01-06 02:17:52 +00:00
parent 4d1c2c4939
commit 4a2b60aeca
2 changed files with 99 additions and 11 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}