Skip to content

Conversation

@michaelsproul
Copy link
Member

@michaelsproul michaelsproul commented Nov 6, 2025

Proposed Changes

Optimise dequeuing of attestations in fork choice by avoiding reallocating the queue after every dequeue.

The included benchmarks shows that this takes ~35% off the runtime of dequeue_attestations over multiple slots:

dequeue_attestations/93750
                        time:   [10.603 ms 10.607 ms 10.613 ms]
                        change: [-34.468% -34.367% -34.274%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe

The baseline branch with just the benchmark added to unstable can be found here: https://github.com/michaelsproul/lighthouse/tree/dequeue-attestation-baseline-benchmark

Additional Info

I played around with several VecDeque-based implementations. The one with partition_point and rotate_left/split_off is substantially faster than using pop_front or drain.

The risk of this PR is that the allocation for queued_attestations is never shrunk/contracted, so in unusual circumstances it could grow very large. We could consider adding a hard cap on the number of items in the queue, as we've also had this queue grow in an unbounded way during times of memory corruption:

@michaelsproul michaelsproul added work-in-progress PR is a work-in-progress optimization Something to make Lighthouse run more efficiently. fork-choice labels Nov 6, 2025
PersistedForkChoice {
proto_array: self.proto_array().as_ssz_container(),
queued_attestations: self.queued_attestations().to_vec(),
queued_attestations: self.queued_attestations().iter().cloned().collect(),
Copy link
Member Author

Choose a reason for hiding this comment

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

This is potentially slower than if we implemented Encode for VecDeque. The conversion from VecDeque to Vec is non-trivial in most cases where the VecDeque doesn't start at index 0:

https://doc.rust-lang.org/std/collections/struct.VecDeque.html#impl-From%3CVecDeque%3CT,+A%3E%3E-for-Vec%3CT,+A%3E

I'll check fork choice persistence times to make sure this isn't having a substantial impact.

fc_store,
proto_array,
queued_attestations: persisted.queued_attestations,
queued_attestations: persisted.queued_attestations.into(),
Copy link
Member Author

Choose a reason for hiding this comment

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

This conversion Vec => VecDeque is trivial and cheap, but only happens once on startup anyway.

use types::{Epoch, Hash256, Slot};

fn all_benches(c: &mut Criterion) {
let num_attestations = 1_500_000_usize / 16;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is ~2 slots worth of attestations on a 1.5M validator network

@michaelsproul michaelsproul added ready-for-review The code is ready for review and removed work-in-progress PR is a work-in-progress labels Nov 10, 2025
@mergify
Copy link

mergify bot commented Nov 10, 2025

Some required checks have failed. Could you please take a look @michaelsproul? 🙏

@mergify mergify bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Nov 10, 2025
@michaelsproul michaelsproul added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Nov 10, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR optimizes the attestation queue management in fork choice by replacing Vec<QueuedAttestation> with VecDeque<QueuedAttestation> and improving the dequeue_attestations algorithm to preserve allocation capacity and avoid unnecessary reallocations.

Key Changes

  • Replaced Vec with VecDeque for the queued attestations data structure across fork choice components
  • Optimized dequeue_attestations function using partition_point, rotate_left, and split_off to maintain capacity while removing old attestations
  • Made dequeue_attestations public and exported it for external use, including in benchmarks

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
consensus/fork_choice/src/fork_choice.rs Core implementation changes: converted queued_attestations to VecDeque, optimized dequeue logic with capacity preservation, made QueuedAttestation fields public, added Debug derive
consensus/fork_choice/src/lib.rs Added public export of dequeue_attestations function
consensus/fork_choice/tests/tests.rs Updated test helper function signature to use VecDeque reference
consensus/fork_choice/benches/benches.rs Added comprehensive benchmark testing dequeue performance with realistic attestation volumes
consensus/fork_choice/Cargo.toml Added criterion dev-dependency and benchmark harness configuration
Cargo.lock Updated with criterion dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@michaelsproul
Copy link
Member Author

dequeue_attestations is no longer visible in the profiling flamegraph after this change 🔥

@mergify
Copy link

mergify bot commented Nov 10, 2025

Some required checks have failed. Could you please take a look @michaelsproul? 🙏

@mergify mergify bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Nov 10, 2025
@michaelsproul michaelsproul added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Nov 10, 2025
@mergify
Copy link

mergify bot commented Nov 12, 2025

This pull request has merge conflicts. Could you please resolve them @michaelsproul? 🙏

@mergify mergify bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Nov 12, 2025
@michaelsproul michaelsproul added v8.1.0 Post-Fulu release ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Nov 12, 2025
let to_pop = queued_attestations.partition_point(|a| a.slot < current_slot);

// Rotate the entries to remove into the *end* of the vec deque.
queued_attestations.rotate_left(to_pop);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not change self.queued_attestations into a map of Slot -> Vec< QueuedAttestation> and when that slot arrives we just remove the entire vec? No need to rotate or split_off

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea, I'll try it and see how the benchmark looks

Copy link
Member Author

Choose a reason for hiding this comment

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

It will have the same problem of re-allocating memory for the Vecs though.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's 4x slower 💀

dequeue_attestations/93750
                        time:   [41.906 ms 41.928 ms 41.953 ms]
Found 8 outliers among 100 measurements (8.00%)
  5 (5.00%) high mild
  3 (3.00%) high severe

Impl here: michaelsproul@daf3617.

One of the problems is that I didn't try optimising out the concatenation of Vecs. dequeue_attestations could definitely return like a Vec<Vec<_>> or something, but I didn't bother refactoring any further, because I think the lack of memory-reuse just makes it worse than the VecDeque.

@michaelsproul michaelsproul added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Nov 26, 2025
@michaelsproul
Copy link
Member Author

Putting this back to waiting-for-author so we can address the unbounded memory growth. Going to try the hashmap approach with pre-allocated Vecs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork-choice optimization Something to make Lighthouse run more efficiently. v8.1.0 Post-Fulu release waiting-on-author The reviewer has suggested changes and awaits thier implementation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants