Skip to content

perf(epoch): replace AutoHashMap with array lookup in reward/penalty caches#286

Open
lodekeeper-z wants to merge 4 commits intoChainSafe:mainfrom
lodekeeper-z:perf/array-lookup-epoch-cache
Open

perf(epoch): replace AutoHashMap with array lookup in reward/penalty caches#286
lodekeeper-z wants to merge 4 commits intoChainSafe:mainfrom
lodekeeper-z:perf/array-lookup-epoch-cache

Conversation

@lodekeeper-z
Copy link
Copy Markdown
Contributor

Summary

Replace heap-allocated AutoHashMap keyed by effective_balance_increment with stack-allocated fixed-size arrays in three epoch processing functions:

  • getRewardsAndPenaltiesAltair
  • getAttestationDeltas
  • processSlashings

Motivation

effective_balance_increment is bounded: max 32 pre-Electra, max 2048 post-Electra. The existing AutoHashMap performs unnecessary heap allocation and hashing in the hot per-validator loops during epoch transitions (~1.1M validators on mainnet).

A fixed-size [max_increment]?T array provides:

  • O(1) lookup (direct indexing vs hash probe)
  • Zero allocation (stack-allocated, no heap pressure)
  • Better cache locality (contiguous memory)

Changes

  • Replace std.AutoHashMap with [max_increment]?RewardPenaltyItem / [max_increment]?u64 arrays
  • Remove now-unused allocator parameters from getRewardsAndPenaltiesAltair, getAttestationDeltas, processSlashings, and getRewardsAndPenalties
  • Update all call sites

Testing

All test:state_transition tests pass.

Refs: #243

🤖 Generated with AI assistance

…caches

Replace heap-allocated AutoHashMap keyed by effective_balance_increment
with stack-allocated fixed-size arrays in three epoch processing functions:
- getRewardsAndPenaltiesAltair
- getAttestationDeltas
- processSlashings

effective_balance_increment is bounded (max 32 pre-Electra, max 2048
post-Electra), so a fixed array provides O(1) lookup with zero allocation
overhead. This eliminates hashing and heap allocation in the hot per-validator
loops during epoch transitions.

Also removes now-unused allocator parameters from these functions,
simplifying call signatures.

Refs: ChainSafe#243

🤖 Generated with AI assistance
@lodekeeper-z lodekeeper-z requested a review from a team as a code owner March 27, 2026 07:12
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request optimizes the performance of epoch processing by replacing dynamic hash map lookups with direct array indexing. By leveraging the bounded nature of effective balance increments, the changes reduce memory pressure and improve cache locality in hot validator loops, resulting in more efficient state transitions.

Highlights

  • Performance Optimization: Replaced heap-allocated std.AutoHashMap with stack-allocated fixed-size arrays in critical epoch processing functions to eliminate unnecessary allocations and hashing overhead.
  • API Cleanup: Removed redundant allocator parameters from getRewardsAndPenaltiesAltair, getAttestationDeltas, processSlashings, and getRewardsAndPenalties as they are no longer needed for cache management.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes epoch state transitions by replacing dynamic std.AutoHashMap caches with fixed-size arrays in getAttestationDeltas, getRewardsAndPenaltiesAltair, and processSlashings. These changes eliminate the need for an Allocator in several functions, reducing overhead from hashing and memory allocation in performance-critical validator loops. The cache sizes are determined at compile-time based on the Electra preset. I have no feedback to provide as the changes align with the performance goals of the repository.

processSlashings signature changed in this PR to remove the allocator
parameter (no longer needed after switching from AutoHashMap to array
lookup). Update the two call sites in bench/state_transition/process_epoch.zig.

🤖 Generated with AI assistance
…enalties signatures

processSlashings no longer takes an allocator param.
getRewardsAndPenalties no longer takes an allocator param.

🤖 Generated with AI assistance
GrapeBaBa
GrapeBaBa previously approved these changes Mar 28, 2026
// effectiveBalance is a multiple of EFFECTIVE_BALANCE_INCREMENT and bounded by max effective balance,
// so effective_balance_increment has a small range (0..max_increment). Use a fixed array for O(1) lookup
// instead of a HashMap — avoids allocation and hashing overhead in the hot validator loop.
const max_increment = comptime preset.MAX_EFFECTIVE_BALANCE_ELECTRA / preset.EFFECTIVE_BALANCE_INCREMENT + 1;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAttestationDeltas() is supposed for phase0 so need to use MAX_EFFECTIVE_BALANCE instead

also would be great to add a comment for this function stating phase0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f728c3. Changed to use MAX_EFFECTIVE_BALANCE (32 increments) since this function is phase0-only. Added comment noting it's phase0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f728c3. Changed to use MAX_EFFECTIVE_BALANCE (32 increments) since this function is phase0-only. Added comment noting it's phase0.

// so effective_balance_increment has a small range (0..max_increment). Use a fixed array for O(1) lookup
// instead of a HashMap — avoids allocation and hashing overhead in the hot validator loop.
const max_increment = comptime preset.MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT + 1;
var reward_penalty_item_cache: [max_increment]?RewardPenaltyItem = .{null} ** max_increment;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for pre-electra, use MAX_EFFECTIVE_BALANCE to save memory

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f728c3. Now uses comptime fork selection: MAX_EFFECTIVE_BALANCE (33 slots) for pre-Electra forks, MAX_EFFECTIVE_BALANCE_ELECTRA (2049 slots) for Electra+. Same for processSlashings.

@@ -74,7 +73,7 @@ pub fn getRewardsAndPenaltiesAltair(

const effective_balance_increment = effective_balance_increments[i];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a little bit unsafe because effective_balance_increment is modeled as u16 pub const EffectiveBalanceIncrements = std.ArrayList(u16);
maybe switch it to u11 to match 2048 as in electra?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern. The u16 type allows values up to 65535 but the array is at most 2049. With the fork-aware sizing now in place, the arrays are even smaller (33 for phase0/pre-Electra).

Changing EffectiveBalanceIncrements from ArrayList(u16) to a narrower type (u12 would cover 0..2048) is a broader change touching epoch_cache, epoch_transition_cache, and all consumers. Worth doing as a separate PR — want me to open one?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go for it @lodekeeper-z

- getAttestationDeltas (phase0 only): use MAX_EFFECTIVE_BALANCE (32 increments)
- getRewardsAndPenaltiesAltair: use comptime fork to select max (32 pre-Electra, 2048 post)
- processSlashings: same fork-aware sizing

Addresses review feedback from twoeths on PR ChainSafe#286.

🤖 Generated with AI assistance
Copy link
Copy Markdown
Collaborator

@twoeths twoeths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me

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