Skip to content

Commit 407e9e0

Browse files
authored
perf: Use a Vec for CycleHeads (#760)
* Use a `Vec` for `CycleHeads` The number of cycle participants is generally low so a hashset likely has more overhead compared to a simple vec * Use `CycleHeads` type in `VerifyResult` * Simplify some `report_tracked_read` calls * Simplify `CycleHeads` ref iterator
1 parent 1bbf4c2 commit 407e9e0

8 files changed

+135
-90
lines changed

src/active_query.rs

+11
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ impl ActiveQuery {
9494
self.cycle_heads.extend(cycle_heads);
9595
}
9696

97+
pub(super) fn add_read_simple(
98+
&mut self,
99+
input: InputDependencyIndex,
100+
durability: Durability,
101+
revision: Revision,
102+
) {
103+
self.input_outputs.insert(QueryEdge::Input(input));
104+
self.durability = self.durability.min(durability);
105+
self.changed_at = self.changed_at.max(revision);
106+
}
107+
97108
pub(super) fn add_untracked_read(&mut self, changed_at: Revision) {
98109
self.untracked_read = true;
99110
self.durability = Durability::MIN;

src/cycle.rs

+50-29
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
//! for each iteration of the outer cycle.
5151
5252
use crate::key::DatabaseKeyIndex;
53-
use rustc_hash::FxHashSet;
5453

5554
/// The maximum number of times we'll fixpoint-iterate before panicking.
5655
///
@@ -91,7 +90,8 @@ pub enum CycleRecoveryStrategy {
9190
/// the cycle head(s) (can be plural in case of nested cycles) representing the cycles it is part
9291
/// of. This struct tracks these cycle heads.
9392
#[derive(Clone, Debug, Default)]
94-
pub(crate) struct CycleHeads(Option<Box<FxHashSet<DatabaseKeyIndex>>>);
93+
#[allow(clippy::box_collection)]
94+
pub struct CycleHeads(Option<Box<Vec<DatabaseKeyIndex>>>);
9595

9696
impl CycleHeads {
9797
pub(crate) fn is_empty(&self) -> bool {
@@ -105,52 +105,62 @@ impl CycleHeads {
105105
}
106106

107107
pub(crate) fn remove(&mut self, value: &DatabaseKeyIndex) -> bool {
108-
if let Some(cycle_heads) = self.0.as_mut() {
109-
let found = cycle_heads.remove(value);
110-
if found && cycle_heads.is_empty() {
111-
self.0.take();
108+
let Some(cycle_heads) = &mut self.0 else {
109+
return false;
110+
};
111+
let found = cycle_heads.iter().position(|&head| head == *value);
112+
let Some(found) = found else { return false };
113+
cycle_heads.swap_remove(found);
114+
if cycle_heads.is_empty() {
115+
self.0.take();
116+
}
117+
true
118+
}
119+
120+
#[inline]
121+
pub(crate) fn insert_into(self, cycle_heads: &mut Vec<DatabaseKeyIndex>) {
122+
if let Some(heads) = self.0 {
123+
for head in *heads {
124+
if !cycle_heads.contains(&head) {
125+
cycle_heads.push(head);
126+
}
112127
}
113-
found
114-
} else {
115-
false
116128
}
117129
}
118-
}
119130

120-
impl std::iter::Extend<DatabaseKeyIndex> for CycleHeads {
121-
fn extend<T: IntoIterator<Item = DatabaseKeyIndex>>(&mut self, iter: T) {
122-
let mut iter = iter.into_iter();
123-
if let Some(first) = iter.next() {
124-
let heads = self.0.get_or_insert(Box::new(FxHashSet::default()));
125-
heads.insert(first);
126-
heads.extend(iter)
131+
pub(crate) fn extend(&mut self, other: &Self) {
132+
if let Some(other) = &other.0 {
133+
let heads = &mut **self.0.get_or_insert_with(|| Box::new(Vec::new()));
134+
heads.reserve(other.len());
135+
other.iter().for_each(|&head| {
136+
if !heads.contains(&head) {
137+
heads.push(head);
138+
}
139+
});
127140
}
128141
}
129142
}
130143

131-
impl std::iter::IntoIterator for CycleHeads {
144+
impl IntoIterator for CycleHeads {
132145
type Item = DatabaseKeyIndex;
133-
type IntoIter = std::collections::hash_set::IntoIter<Self::Item>;
146+
type IntoIter = <Vec<Self::Item> as IntoIterator>::IntoIter;
134147

135148
fn into_iter(self) -> Self::IntoIter {
136149
self.0.map(|heads| *heads).unwrap_or_default().into_iter()
137150
}
138151
}
139152

140-
// This type can be removed once MSRV is 1.83+ and we have Default for hashset iterators.
141-
pub(crate) struct CycleHeadsIter<'a>(
142-
Option<std::collections::hash_set::Iter<'a, DatabaseKeyIndex>>,
143-
);
153+
pub struct CycleHeadsIter<'a>(std::slice::Iter<'a, DatabaseKeyIndex>);
144154

145155
impl Iterator for CycleHeadsIter<'_> {
146156
type Item = DatabaseKeyIndex;
147157

148158
fn next(&mut self) -> Option<Self::Item> {
149-
self.0.as_mut()?.next().copied()
159+
self.0.next().copied()
150160
}
151161

152162
fn last(self) -> Option<Self::Item> {
153-
self.0?.last().copied()
163+
self.0.last().copied()
154164
}
155165
}
156166

@@ -161,12 +171,23 @@ impl<'a> std::iter::IntoIterator for &'a CycleHeads {
161171
type IntoIter = CycleHeadsIter<'a>;
162172

163173
fn into_iter(self) -> Self::IntoIter {
164-
CycleHeadsIter(self.0.as_ref().map(|heads| heads.iter()))
174+
CycleHeadsIter(
175+
self.0
176+
.as_ref()
177+
.map(|heads| heads.iter())
178+
.unwrap_or_default(),
179+
)
180+
}
181+
}
182+
183+
impl From<DatabaseKeyIndex> for CycleHeads {
184+
fn from(value: DatabaseKeyIndex) -> Self {
185+
Self(Some(Box::new(vec![value])))
165186
}
166187
}
167188

168-
impl From<FxHashSet<DatabaseKeyIndex>> for CycleHeads {
169-
fn from(value: FxHashSet<DatabaseKeyIndex>) -> Self {
189+
impl From<Vec<DatabaseKeyIndex>> for CycleHeads {
190+
fn from(value: Vec<DatabaseKeyIndex>) -> Self {
170191
Self(if value.is_empty() {
171192
None
172193
} else {
@@ -175,4 +196,4 @@ impl From<FxHashSet<DatabaseKeyIndex>> for CycleHeads {
175196
}
176197
}
177198

178-
pub(crate) static EMPTY_CYCLE_HEADS: CycleHeads = CycleHeads(None);
199+
pub(crate) const EMPTY_CYCLE_HEADS: CycleHeads = CycleHeads(None);

src/function/maybe_changed_after.rs

+22-17
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use crate::{
22
accumulator::accumulated_map::InputAccumulatedValues,
3-
cycle::CycleRecoveryStrategy,
3+
cycle::{CycleHeads, CycleRecoveryStrategy},
44
key::DatabaseKeyIndex,
55
table::sync::ClaimResult,
66
zalsa::{MemoIngredientIndex, Zalsa, ZalsaDatabase},
77
zalsa_local::{ActiveQueryGuard, QueryEdge, QueryOrigin},
88
AsDynDatabase as _, Id, Revision,
99
};
10-
use rustc_hash::FxHashSet;
1110
use std::sync::atomic::Ordering;
1211

1312
use super::{memo::Memo, Configuration, IngredientImpl};
@@ -22,9 +21,9 @@ pub enum VerifyResult {
2221
/// The first inner value tracks whether the memo or any of its dependencies have an
2322
/// accumulated value.
2423
///
25-
/// Database keys in the hashset represent cycle heads encountered in validation; don't mark
24+
/// The second is the cycle heads encountered in validation; don't mark
2625
/// memos verified until we've iterated the full cycle to ensure no inputs changed.
27-
Unchanged(InputAccumulatedValues, FxHashSet<DatabaseKeyIndex>),
26+
Unchanged(InputAccumulatedValues, CycleHeads),
2827
}
2928

3029
impl VerifyResult {
@@ -37,7 +36,7 @@ impl VerifyResult {
3736
}
3837

3938
pub(crate) fn unchanged() -> Self {
40-
Self::Unchanged(InputAccumulatedValues::Empty, FxHashSet::default())
39+
Self::Unchanged(InputAccumulatedValues::Empty, CycleHeads::default())
4140
}
4241
}
4342

@@ -69,7 +68,7 @@ where
6968
} else {
7069
VerifyResult::Unchanged(
7170
memo.revisions.accumulated_inputs.load(),
72-
FxHashSet::default(),
71+
CycleHeads::default(),
7372
)
7473
};
7574
}
@@ -112,7 +111,7 @@ where
112111
CycleRecoveryStrategy::Fixpoint => {
113112
return Some(VerifyResult::Unchanged(
114113
InputAccumulatedValues::Empty,
115-
FxHashSet::from_iter([database_key_index]),
114+
CycleHeads::from(database_key_index),
116115
));
117116
}
118117
},
@@ -158,7 +157,7 @@ where
158157
Some(_) => InputAccumulatedValues::Any,
159158
None => memo.revisions.accumulated_inputs.load(),
160159
},
161-
FxHashSet::default(),
160+
CycleHeads::default(),
162161
)
163162
});
164163
}
@@ -240,13 +239,12 @@ where
240239
zalsa: &Zalsa,
241240
memo: &Memo<C::Output<'_>>,
242241
) -> bool {
243-
for cycle_head in &memo.revisions.cycle_heads {
244-
if zalsa
242+
if (&memo.revisions.cycle_heads).into_iter().any(|cycle_head| {
243+
zalsa
245244
.lookup_ingredient(cycle_head.ingredient_index)
246245
.is_provisional_cycle_head(db.as_dyn_database(), cycle_head.key_index)
247-
{
248-
return false;
249-
}
246+
}) {
247+
return false;
250248
}
251249
// Relaxed is sufficient here because there are no other writes we need to ensure have
252250
// happened before marking this memo as verified-final.
@@ -282,7 +280,7 @@ where
282280
return VerifyResult::Changed;
283281
}
284282

285-
let mut cycle_heads = FxHashSet::default();
283+
let mut cycle_heads = vec![];
286284
loop {
287285
let inputs = match &old_memo.revisions.origin {
288286
QueryOrigin::Assigned(_) => {
@@ -324,7 +322,7 @@ where
324322
{
325323
VerifyResult::Changed => return VerifyResult::Changed,
326324
VerifyResult::Unchanged(input_accumulated, cycles) => {
327-
cycle_heads.extend(cycles);
325+
cycles.insert_into(&mut cycle_heads);
328326
inputs |= input_accumulated;
329327
}
330328
}
@@ -384,7 +382,11 @@ where
384382
// from cycle heads. We will handle our own memo (and the rest of our cycle) on a
385383
// future iteration; first the outer cycle head needs to verify itself.
386384

387-
let in_heads = cycle_heads.remove(&database_key_index);
385+
let in_heads = cycle_heads
386+
.iter()
387+
.position(|&head| head == database_key_index)
388+
.inspect(|&head| _ = cycle_heads.swap_remove(head))
389+
.is_some();
388390

389391
if cycle_heads.is_empty() {
390392
old_memo.mark_as_verified(db, zalsa.current_revision(), database_key_index, inputs);
@@ -398,7 +400,10 @@ where
398400
continue;
399401
}
400402
}
401-
return VerifyResult::Unchanged(InputAccumulatedValues::Empty, cycle_heads);
403+
return VerifyResult::Unchanged(
404+
InputAccumulatedValues::Empty,
405+
CycleHeads::from(cycle_heads),
406+
);
402407
}
403408
}
404409
}

src/function/memo.rs

+26-23
Original file line numberDiff line numberDiff line change
@@ -177,29 +177,32 @@ impl<V> Memo<V> {
177177
database_key_index: DatabaseKeyIndex,
178178
) -> bool {
179179
let mut retry = false;
180-
for head in self.cycle_heads() {
181-
if head == database_key_index {
182-
continue;
183-
}
184-
let ingredient = db.zalsa().lookup_ingredient(head.ingredient_index);
185-
if !ingredient.is_provisional_cycle_head(db, head.key_index) {
186-
// This cycle is already finalized, so we don't need to wait on it;
187-
// keep looping through cycle heads.
188-
retry = true;
189-
continue;
190-
}
191-
if ingredient.wait_for(db, head.key_index) {
192-
// There's a new memo available for the cycle head; fetch our own
193-
// updated memo and see if it's still provisional or if the cycle
194-
// has resolved.
195-
retry = true;
196-
continue;
197-
} else {
198-
// We hit a cycle blocking on the cycle head; this means it's in
199-
// our own active query stack and we are responsible to resolve the
200-
// cycle, so go ahead and return the provisional memo.
201-
return false;
202-
}
180+
let hit_cycle = self
181+
.cycle_heads()
182+
.into_iter()
183+
.filter(|&head| head != database_key_index)
184+
.any(|head| {
185+
let ingredient = db.zalsa().lookup_ingredient(head.ingredient_index);
186+
if !ingredient.is_provisional_cycle_head(db, head.key_index) {
187+
// This cycle is already finalized, so we don't need to wait on it;
188+
// keep looping through cycle heads.
189+
retry = true;
190+
false
191+
} else if ingredient.wait_for(db, head.key_index) {
192+
// There's a new memo available for the cycle head; fetch our own
193+
// updated memo and see if it's still provisional or if the cycle
194+
// has resolved.
195+
retry = true;
196+
false
197+
} else {
198+
// We hit a cycle blocking on the cycle head; this means it's in
199+
// our own active query stack and we are responsible to resolve the
200+
// cycle, so go ahead and return the provisional memo.
201+
true
202+
}
203+
});
204+
if hit_cycle {
205+
return false;
203206
}
204207
// If `retry` is `true`, all our cycle heads (barring ourself) are complete; re-fetch
205208
// and we should get a non-provisional memo. If we get here and `retry` is still

src/input.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ pub mod singleton;
1111
use input_field::FieldIngredientImpl;
1212

1313
use crate::{
14-
accumulator::accumulated_map::InputAccumulatedValues,
15-
cycle::{CycleRecoveryStrategy, EMPTY_CYCLE_HEADS},
14+
cycle::CycleRecoveryStrategy,
1615
function::VerifyResult,
1716
id::{AsId, FromIdWithDb},
1817
ingredient::{fmt_index, Ingredient},
@@ -178,12 +177,10 @@ impl<C: Configuration> IngredientImpl<C> {
178177
let id = id.as_id();
179178
let value = Self::data(zalsa, id);
180179
let stamp = &value.stamps[field_index];
181-
zalsa_local.report_tracked_read(
180+
zalsa_local.report_tracked_read_simple(
182181
InputDependencyIndex::new(field_ingredient_index, id),
183182
stamp.durability,
184183
stamp.changed_at,
185-
InputAccumulatedValues::Empty,
186-
&EMPTY_CYCLE_HEADS,
187184
);
188185
&value.fields
189186
}

src/interned.rs

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use dashmap::SharedValue;
22

3-
use crate::accumulator::accumulated_map::InputAccumulatedValues;
4-
use crate::cycle::EMPTY_CYCLE_HEADS;
53
use crate::durability::Durability;
64
use crate::function::VerifyResult;
75
use crate::ingredient::fmt_index;
@@ -180,12 +178,10 @@ where
180178
C::Fields<'db>: HashEqLike<Key>,
181179
{
182180
let zalsa_local = db.zalsa_local();
183-
zalsa_local.report_tracked_read(
181+
zalsa_local.report_tracked_read_simple(
184182
InputDependencyIndex::for_table(self.ingredient_index),
185183
Durability::MAX,
186184
self.reset_at,
187-
InputAccumulatedValues::Empty,
188-
&EMPTY_CYCLE_HEADS,
189185
);
190186

191187
// Optimization to only get read lock on the map if the data has already been interned.

0 commit comments

Comments
 (0)