Skip to content

starknet_patricia,starknet_patricia_storage: cut redundant hashmap work in per-block hot paths#14853

Open
gkaempfer wants to merge 1 commit into
mainfrom
claude/perf/starknet-patricia-hashmap-cost-cuts-29170
Open

starknet_patricia,starknet_patricia_storage: cut redundant hashmap work in per-block hot paths#14853
gkaempfer wants to merge 1 commit into
mainfrom
claude/perf/starknet-patricia-hashmap-cost-cuts-29170

Conversation

@gkaempfer

Copy link
Copy Markdown
Contributor

Summary

Follow-up performance review of #14850 (starknet_patricia_storage: remove redundant copy in create_db_key), scoped to two more hot-path hashmap inefficiencies found while auditing the surrounding storage/commit code.

1. RocksDbStorage::mset — redundant hashmap re-lookup per key

// before
for key in key_to_value.keys() {
    batch.put(&key.0, &key_to_value[key].0);
}
// after
for (key, value) in key_to_value.iter() {
    batch.put(&key.0, &value.0);
}

mset iterated .keys() and then re-looked-up each value via key_to_value[key] (the Index impl), hashing and probing the map a second time for every one of the O(N_nodes) entries written per block. .iter() yields (key, value) directly — same pattern multi_set_and_delete right below it already uses.

2. FilledTreeImpl::initialize_filled_tree_output_map_with_placeholders — lost capacity hint after the OnceLock refactor

This function used to build its output map with an explicit with_capacity hint (added in #14641 to avoid O(log N) rehash-on-grow). It has since been rewritten to updated_skeleton.get_nodes().filter_map(...).collect() as part of switching placeholders from Mutex<Option<T>> to OnceLock<T>. filter_map's size_hint() lower bound is always 0 (filtering may drop every element), so collect/extend can no longer preallocate, silently reintroducing the rehashing cost the earlier fix removed.

// after
let nodes = updated_skeleton.get_nodes();
let mut filled_tree_output_map = HashMap::with_capacity(nodes.size_hint().0);
filled_tree_output_map.extend(nodes.filter_map(|(index, node)| match node {
    UpdatedSkeletonNode::UnmodifiedSubTree(_) => None,
    _ => Some((*index, OnceLock::new())),
}));
filled_tree_output_map

get_nodes() is backed by a plain HashMap::iter(), so its size_hint().0 is exact — reserving on the unfiltered count is a safe (tight) upper bound for the filtered map, with identical resulting contents.

Test plan

  • cargo build -p starknet_patricia -p starknet_patricia_storage
  • SEED=0 cargo test -p starknet_patricia — 107 passed
  • SEED=0 cargo test -p starknet_patricia_storage — 8 passed, including storage_test::test_storage_concurrent_access::case_1_rocksdb_storage which exercises the mset path
  • Reviewed by an independent Opus pass: confirmed filter_map's size_hint semantics, confirmed get_nodes()'s size_hint is exact, confirmed no double-consume/behavior change, no blocking or suggestion findings

🤖 Generated with Claude Code


Generated by Claude Code

…rk in per-block hot paths

Follow-up performance review of #14850.

- RocksDbStorage::mset iterated key_to_value.keys() and then re-looked-up
  each value via key_to_value[key], hashing and probing the map twice per
  key on every block's batch write. Use .iter() to get (key, value) pairs
  directly, as multi_set_and_delete already does.

- FilledTreeImpl::initialize_filled_tree_output_map_with_placeholders builds
  its output map via `.filter_map(...).collect()`. filter_map's size_hint
  lower bound is always 0, so collect (and the Extend impl backing it) could
  not preallocate, reintroducing the O(log N) rehash-on-grow cost that a
  prior fix (#14641) had removed from this same function before it was
  rewritten to use OnceLock placeholders. Reserve capacity using the
  (exact) unfiltered node count instead.

🤖 Generated with Claude Code
@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Behavior-preserving micro-optimizations on iteration and map capacity; no API or semantics changes.

Overview
Per-block Patricia and RocksDB paths shed redundant HashMap work without changing behavior.

RocksDbStorage::mset now walks key_to_value.iter() when building the write batch, so each entry is hashed once instead of iterating keys and re-looking up values via key_to_value[key] (aligned with multi_set_and_delete below it).

initialize_filled_tree_output_map_with_placeholders again preallocates the filled-tree placeholder map: it reserves using the exact size from get_nodes() (a HashMap iterator), then extends the same filter_map logic. A plain collect() after filter_map could not preallocate because the adapter’s size hint lower bound is always 0.

Reviewed by Cursor Bugbot for commit 4908dc1. Bugbot is set up for automated code reviews on this repo. Configure here.

@reviewable-StarkWare

Copy link
Copy Markdown

This change is Reviewable

@gkaempfer gkaempfer self-assigned this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants