From 95062494d2b57be1d743bdb5f648c43e3342b5e2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 6 Oct 2025 21:42:51 -0400 Subject: [PATCH] automata: make PikeVM cache initialization lazy Prior to the advent of regex-automata, the PikeVM would decide how much space it needed at the beginning of every search. In regex-automata, we did away with that check at search time and moved it to the time at which the cache is constructed. (The inputs to the sizing are currently invariant in regex-automata, as they were in the old regex crate.) The downside of this is that we create the caches for each regex engine eagerly. So even if we never call the PikeVM (which is actually quite common, since the lazy DFA handles mostly everything), we end up paying for the memory of its cache. In many cases, this memory is likely negligible, but it can be substantial if there are a lot of capture groups, even if they aren't used. As in #1116. We fix this by just re-arranging the meta regex engine wrappers to avoid eagerly creating caches. Instead, they are only initialized when they are actually needed. This ends up making memory usage a bit less than `regex 1.7.3`. Fixes #1116 --- CHANGELOG.md | 2 + regex-automata/src/meta/wrappers.rs | 57 +++++++++++------------------ 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8525909..8d0a0e036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ TODO Bug fixes: +* [BUG #1116](https://github.com/rust-lang/regex/issues/1116): +Fixes a memory usage regression for large regexes (introduced in `regex 1.9`). * [BUG #1165](https://github.com/rust-lang/regex/issues/1083): Fixes a panic in the lazy DFA (can only occur for especially large regexes). diff --git a/regex-automata/src/meta/wrappers.rs b/regex-automata/src/meta/wrappers.rs index fd1d5a144..6651cb907 100644 --- a/regex-automata/src/meta/wrappers.rs +++ b/regex-automata/src/meta/wrappers.rs @@ -58,7 +58,7 @@ impl PikeVM { } pub(crate) fn create_cache(&self) -> PikeVMCache { - PikeVMCache::new(self) + PikeVMCache::none() } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -93,7 +93,7 @@ impl PikeVMEngine { cache: &mut PikeVMCache, input: &Input<'_>, ) -> bool { - self.0.is_match(cache.0.as_mut().unwrap(), input.clone()) + self.0.is_match(cache.get(&self.0), input.clone()) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -103,7 +103,7 @@ impl PikeVMEngine { input: &Input<'_>, slots: &mut [Option], ) -> Option { - self.0.search_slots(cache.0.as_mut().unwrap(), input, slots) + self.0.search_slots(cache.get(&self.0), input, slots) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -113,11 +113,7 @@ impl PikeVMEngine { input: &Input<'_>, patset: &mut PatternSet, ) { - self.0.which_overlapping_matches( - cache.0.as_mut().unwrap(), - input, - patset, - ) + self.0.which_overlapping_matches(cache.get(&self.0), input, patset) } } @@ -129,17 +125,17 @@ impl PikeVMCache { PikeVMCache(None) } - pub(crate) fn new(builder: &PikeVM) -> PikeVMCache { - PikeVMCache(Some(builder.get().0.create_cache())) - } - pub(crate) fn reset(&mut self, builder: &PikeVM) { - self.0.as_mut().unwrap().reset(&builder.get().0); + self.get(&builder.get().0).reset(&builder.get().0); } pub(crate) fn memory_usage(&self) -> usize { self.0.as_ref().map_or(0, |c| c.memory_usage()) } + + fn get(&mut self, vm: &pikevm::PikeVM) -> &mut pikevm::Cache { + self.0.get_or_insert_with(|| vm.create_cache()) + } } #[derive(Debug)] @@ -155,7 +151,7 @@ impl BoundedBacktracker { } pub(crate) fn create_cache(&self) -> BoundedBacktrackerCache { - BoundedBacktrackerCache::new(self) + BoundedBacktrackerCache::none() } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -235,9 +231,7 @@ impl BoundedBacktrackerEngine { // OK because we only permit access to this engine when we know // the haystack is short enough for the backtracker to run without // reporting an error. - self.0 - .try_is_match(cache.0.as_mut().unwrap(), input.clone()) - .unwrap() + self.0.try_is_match(cache.get(&self.0), input.clone()).unwrap() } #[cfg(not(feature = "nfa-backtrack"))] { @@ -259,9 +253,7 @@ impl BoundedBacktrackerEngine { // OK because we only permit access to this engine when we know // the haystack is short enough for the backtracker to run without // reporting an error. - self.0 - .try_search_slots(cache.0.as_mut().unwrap(), input, slots) - .unwrap() + self.0.try_search_slots(cache.get(&self.0), input, slots).unwrap() } #[cfg(not(feature = "nfa-backtrack"))] { @@ -304,25 +296,10 @@ impl BoundedBacktrackerCache { } } - pub(crate) fn new( - builder: &BoundedBacktracker, - ) -> BoundedBacktrackerCache { - #[cfg(feature = "nfa-backtrack")] - { - BoundedBacktrackerCache( - builder.0.as_ref().map(|e| e.0.create_cache()), - ) - } - #[cfg(not(feature = "nfa-backtrack"))] - { - BoundedBacktrackerCache(()) - } - } - pub(crate) fn reset(&mut self, builder: &BoundedBacktracker) { #[cfg(feature = "nfa-backtrack")] if let Some(ref e) = builder.0 { - self.0.as_mut().unwrap().reset(&e.0); + self.get(&e.0).reset(&e.0); } } @@ -336,6 +313,14 @@ impl BoundedBacktrackerCache { 0 } } + + #[cfg(feature = "nfa-backtrack")] + fn get( + &mut self, + bb: &backtrack::BoundedBacktracker, + ) -> &mut backtrack::Cache { + self.0.get_or_insert_with(|| bb.create_cache()) + } } #[derive(Debug)]