-
Notifications
You must be signed in to change notification settings - Fork 314
improve performance of recursion guard #1156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
f98b085
5d14e70
3e73e52
3a621b7
3f72522
e1190ed
4b82950
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| use ahash::AHashSet; | ||
| use std::hash::Hash; | ||
|
|
||
| type RecursionKey = ( | ||
| // Identifier for the input object, e.g. the id() of a Python dict | ||
|
|
@@ -13,56 +14,130 @@ type RecursionKey = ( | |
| /// It's used in `validators/definition` to detect when a reference is reused within itself. | ||
| #[derive(Debug, Clone, Default)] | ||
| pub struct RecursionGuard { | ||
| ids: Option<AHashSet<RecursionKey>>, | ||
| ids: SmallContainer<RecursionKey>, | ||
| // depth could be a hashmap {validator_id => depth} but for simplicity and performance it's easier to just | ||
| // use one number for all validators | ||
| depth: u16, | ||
| depth: u8, | ||
| } | ||
|
|
||
| // A hard limit to avoid stack overflows when rampant recursion occurs | ||
| pub const RECURSION_GUARD_LIMIT: u16 = if cfg!(any(target_family = "wasm", all(windows, PyPy))) { | ||
| pub const RECURSION_GUARD_LIMIT: u8 = if cfg!(any(target_family = "wasm", all(windows, PyPy))) { | ||
| // wasm and windows PyPy have very limited stack sizes | ||
| 50 | ||
| 49 | ||
| } else if cfg!(any(PyPy, windows)) { | ||
| // PyPy and Windows in general have more restricted stack space | ||
| 100 | ||
| 99 | ||
| } else { | ||
| 255 | ||
| }; | ||
|
|
||
| impl RecursionGuard { | ||
| // insert a new id into the set, return whether the set already had the id in it | ||
| pub fn contains_or_insert(&mut self, obj_id: usize, node_id: usize) -> bool { | ||
| match self.ids { | ||
| // https://doc.rust-lang.org/std/collections/struct.HashSet.html#method.insert | ||
| // "If the set did not have this value present, `true` is returned." | ||
| Some(ref mut set) => !set.insert((obj_id, node_id)), | ||
| None => { | ||
| let mut set: AHashSet<RecursionKey> = AHashSet::with_capacity(10); | ||
| set.insert((obj_id, node_id)); | ||
| self.ids = Some(set); | ||
| false | ||
| } | ||
| } | ||
| // insert a new value | ||
| // * return `None` if the array/set already had it in it | ||
| // * return `Some(index)` if the array didn't have it in it and it was inserted | ||
| pub fn contains_or_insert(&mut self, obj_id: usize, node_id: usize) -> Option<usize> { | ||
| self.ids.contains_or_insert((obj_id, node_id)) | ||
| } | ||
|
|
||
| // see #143 this is used as a backup in case the identity check recursion guard fails | ||
| #[must_use] | ||
| #[cfg(any(target_family = "wasm", windows, PyPy))] | ||
| pub fn incr_depth(&mut self) -> bool { | ||
| // use saturating_add as it's faster (since there's no error path) | ||
| // and the RECURSION_GUARD_LIMIT check will be hit before it overflows | ||
| debug_assert!(RECURSION_GUARD_LIMIT < 255); | ||
| self.depth = self.depth.saturating_add(1); | ||
| self.depth > RECURSION_GUARD_LIMIT | ||
| } | ||
|
|
||
| #[must_use] | ||
| #[cfg(not(any(target_family = "wasm", windows, PyPy)))] | ||
| pub fn incr_depth(&mut self) -> bool { | ||
| self.depth += 1; | ||
| self.depth >= RECURSION_GUARD_LIMIT | ||
| debug_assert_eq!(RECURSION_GUARD_LIMIT, 255); | ||
| // use checked_add to check if we've hit the limit | ||
| if let Some(depth) = self.depth.checked_add(1) { | ||
| self.depth = depth; | ||
| false | ||
| } else { | ||
| true | ||
| } | ||
| } | ||
|
|
||
| pub fn decr_depth(&mut self) { | ||
| self.depth -= 1; | ||
| // for the same reason as incr_depth, use saturating_sub | ||
| self.depth = self.depth.saturating_sub(1); | ||
| } | ||
|
|
||
| pub fn remove(&mut self, obj_id: usize, node_id: usize, index: usize) { | ||
| self.ids.remove(&(obj_id, node_id), index); | ||
| } | ||
| } | ||
|
|
||
| // trial and error suggests this is a good value, going higher causes array lookups to get significantly slower | ||
| const ARRAY_SIZE: usize = 16; | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| enum SmallContainer<T> { | ||
| Array([Option<T>; ARRAY_SIZE]), | ||
| Set(AHashSet<T>), | ||
| } | ||
|
|
||
| impl<T: Copy> Default for SmallContainer<T> { | ||
| fn default() -> Self { | ||
| Self::Array([None; ARRAY_SIZE]) | ||
| } | ||
| } | ||
|
|
||
| impl<T: Eq + Hash + Clone> SmallContainer<T> { | ||
| // insert a new value | ||
| // * return `None` if the array/set already had it in it | ||
| // * return `Some(index)` if the array didn't have it in it and it was inserted | ||
| pub fn contains_or_insert(&mut self, v: T) -> Option<usize> { | ||
| match self { | ||
| Self::Array(array) => { | ||
| for (index, op_value) in array.iter_mut().enumerate() { | ||
| if let Some(existing) = op_value { | ||
| if existing == &v { | ||
| return None; | ||
| } | ||
| } else { | ||
| *op_value = Some(v); | ||
|
||
| return Some(index); | ||
| } | ||
| } | ||
|
|
||
| // No array slots exist; convert to set | ||
| let mut set: AHashSet<T> = AHashSet::with_capacity(ARRAY_SIZE + 1); | ||
| for existing in array.iter_mut() { | ||
| set.insert(existing.take().unwrap()); | ||
| } | ||
| set.insert(v); | ||
| *self = Self::Set(set); | ||
| // id doesn't matter here as we'll be removing from a set | ||
| Some(0) | ||
| } | ||
| // https://doc.rust-lang.org/std/collections/struct.HashSet.html#method.insert | ||
| // "If the set did not have this value present, `true` is returned." | ||
| Self::Set(set) => { | ||
| if set.insert(v) { | ||
| // again id doesn't matter here as we'll be removing from a set | ||
| Some(0) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn remove(&mut self, obj_id: usize, node_id: usize) { | ||
| match self.ids { | ||
| Some(ref mut set) => { | ||
| set.remove(&(obj_id, node_id)); | ||
| pub fn remove(&mut self, v: &T, index: usize) { | ||
| match self { | ||
| Self::Array(array) => { | ||
| debug_assert!(array[index].as_ref() == Some(v), "remove did not match insert"); | ||
| array[index] = None; | ||
| } | ||
| Self::Set(set) => { | ||
| set.remove(v); | ||
| } | ||
| None => unreachable!(), | ||
| }; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.