Skip to content
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

fix memory usage regression for RegexSet with capture groups #1062

Merged
merged 8 commits into from
Aug 5, 2023
5 changes: 4 additions & 1 deletion regex-automata/src/dfa/dense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,10 @@ impl Builder {
.clone()
// We can always forcefully disable captures because DFAs do not
// support them.
.configure(thompson::Config::new().captures(false))
.configure(
thompson::Config::new()
.which_captures(thompson::WhichCaptures::None),
)
.build_many(patterns)
.map_err(BuildError::nfa)?;
self.build_from_nfa(&nfa)
Expand Down
5 changes: 4 additions & 1 deletion regex-automata/src/hybrid/dfa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3973,7 +3973,10 @@ impl Builder {
.clone()
// We can always forcefully disable captures because DFAs do not
// support them.
.configure(thompson::Config::new().captures(false))
.configure(
thompson::Config::new()
.which_captures(thompson::WhichCaptures::None),
)
.build_many(patterns)
.map_err(BuildError::nfa)?;
self.build_from_nfa(nfa)
Expand Down
91 changes: 90 additions & 1 deletion regex-automata/src/meta/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
strategy::{self, Strategy},
wrappers,
},
nfa::thompson::WhichCaptures,
util::{
captures::{Captures, GroupInfo},
iter,
Expand Down Expand Up @@ -528,7 +529,14 @@ impl Regex {
#[inline]
pub fn is_match<'h, I: Into<Input<'h>>>(&self, input: I) -> bool {
let input = input.into().earliest(true);
self.search_half(&input).is_some()
if self.imp.info.is_impossible(&input) {
return false;
}
let mut guard = self.pool.get();
let result = self.imp.strat.is_match(&mut guard, &input);
// See 'Regex::search' for why we put the guard back explicitly.
PoolGuard::put(guard);
result
}

/// Executes a leftmost search and returns the first match that is found,
Expand Down Expand Up @@ -2429,6 +2437,7 @@ pub struct Config {
utf8_empty: Option<bool>,
autopre: Option<bool>,
pre: Option<Option<Prefilter>>,
which_captures: Option<WhichCaptures>,
nfa_size_limit: Option<Option<usize>>,
onepass_size_limit: Option<Option<usize>>,
hybrid_cache_capacity: Option<usize>,
Expand Down Expand Up @@ -2619,6 +2628,77 @@ impl Config {
Config { pre: Some(pre), ..self }
}

/// Configures what kinds of groups are compiled as "capturing" in the
/// underlying regex engine.
///
/// This is set to [`WhichCaptures::All`] by default. Callers may wish to
/// use [`WhichCaptures::Implicit`] in cases where one wants avoid the
/// overhead of capture states for explicit groups.
///
/// Note that another approach to avoiding the overhead of capture groups
/// is by using non-capturing groups in the regex pattern. That is,
/// `(?:a)` instead of `(a)`. This option is useful when you can't control
/// the concrete syntax but know that you don't need the underlying capture
/// states. For example, using `WhichCaptures::Implicit` will behave as if
/// all explicit capturing groups in the pattern were non-capturing.
///
/// Setting this to `WhichCaptures::None` is usually not the right thing to
/// do. When no capture states are compiled, some regex engines (such as
/// the `PikeVM`) won't be able to report match offsets. This will manifest
/// as no match being found.
///
/// # Example
///
/// This example demonstrates how the results of capture groups can change
/// based on this option. First we show the default (all capture groups in
/// the pattern are capturing):
///
/// ```
/// use regex_automata::{meta::Regex, Match, Span};
///
/// let re = Regex::new(r"foo([0-9]+)bar")?;
/// let hay = "foo123bar";
///
/// let mut caps = re.create_captures();
/// re.captures(hay, &mut caps);
/// assert_eq!(Some(Span::from(0..9)), caps.get_group(0));
/// assert_eq!(Some(Span::from(3..6)), caps.get_group(1));
///
/// Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// And now we show the behavior when we only include implicit capture
/// groups. In this case, we can only find the overall match span, but the
/// spans of any other explicit group don't exist because they are treated
/// as non-capturing. (In effect, when `WhichCaptures::Implicit` is used,
/// there is no real point in using [`Regex::captures`] since it will never
/// be able to report more information than [`Regex::find`].)
///
/// ```
/// use regex_automata::{
/// meta::Regex,
/// nfa::thompson::WhichCaptures,
/// Match,
/// Span,
/// };
///
/// let re = Regex::builder()
/// .configure(Regex::config().which_captures(WhichCaptures::Implicit))
/// .build(r"foo([0-9]+)bar")?;
/// let hay = "foo123bar";
///
/// let mut caps = re.create_captures();
/// re.captures(hay, &mut caps);
/// assert_eq!(Some(Span::from(0..9)), caps.get_group(0));
/// assert_eq!(None, caps.get_group(1));
///
/// Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn which_captures(mut self, which_captures: WhichCaptures) -> Config {
self.which_captures = Some(which_captures);
self
}

/// Sets the size limit, in bytes, to enforce on the construction of every
/// NFA build by the meta regex engine.
///
Expand Down Expand Up @@ -2983,6 +3063,14 @@ impl Config {
self.pre.as_ref().unwrap_or(&None).as_ref()
}

/// Returns the capture configuration, as set by
/// [`Config::which_captures`].
///
/// If it was not explicitly set, then a default value is returned.
pub fn get_which_captures(&self) -> WhichCaptures {
self.which_captures.unwrap_or(WhichCaptures::All)
}

/// Returns NFA size limit, as set by [`Config::nfa_size_limit`].
///
/// If it was not explicitly set, then a default value is returned.
Expand Down Expand Up @@ -3126,6 +3214,7 @@ impl Config {
utf8_empty: o.utf8_empty.or(self.utf8_empty),
autopre: o.autopre.or(self.autopre),
pre: o.pre.or_else(|| self.pre.clone()),
which_captures: o.which_captures.or(self.which_captures),
nfa_size_limit: o.nfa_size_limit.or(self.nfa_size_limit),
onepass_size_limit: o
.onepass_size_limit
Expand Down
131 changes: 125 additions & 6 deletions regex-automata/src/meta/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
regex::{Cache, RegexInfo},
reverse_inner, wrappers,
},
nfa::thompson::{self, NFA},
nfa::thompson::{self, WhichCaptures, NFA},
util::{
captures::{Captures, GroupInfo},
look::LookMatcher,
Expand Down Expand Up @@ -58,6 +58,8 @@ pub(super) trait Strategy:
input: &Input<'_>,
) -> Option<HalfMatch>;

fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool;

fn search_slots(
&self,
cache: &mut Cache,
Expand Down Expand Up @@ -399,6 +401,10 @@ impl<P: PrefilterI> Strategy for Pre<P> {
self.search(cache, input).map(|m| HalfMatch::new(m.pattern(), m.end()))
}

fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
self.search(cache, input).is_some()
}

fn search_slots(
&self,
cache: &mut Cache,
Expand Down Expand Up @@ -452,7 +458,7 @@ impl Core {
.utf8(info.config().get_utf8_empty())
.nfa_size_limit(info.config().get_nfa_size_limit())
.shrink(false)
.captures(true)
.which_captures(info.config().get_which_captures())
.look_matcher(lookm);
let nfa = thompson::Compiler::new()
.configure(thompson_config.clone())
Expand Down Expand Up @@ -499,7 +505,10 @@ impl Core {
// useful with capturing groups in reverse. And of course,
// the lazy DFA ignores capturing groups in all cases.
.configure(
thompson_config.clone().captures(false).reverse(true),
thompson_config
.clone()
.which_captures(WhichCaptures::None)
.reverse(true),
)
.build_many_from_hir(hirs)
.map_err(BuildError::nfa)?;
Expand Down Expand Up @@ -620,6 +629,29 @@ impl Core {
}
}

fn is_match_nofail(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
if let Some(ref e) = self.onepass.get(input) {
trace!(
"using OnePass for is-match search at {:?}",
input.get_span()
);
e.search_slots(&mut cache.onepass, input, &mut []).is_some()
} else if let Some(ref e) = self.backtrack.get(input) {
trace!(
"using BoundedBacktracker for is-match search at {:?}",
input.get_span()
);
e.is_match(&mut cache.backtrack, input)
} else {
trace!(
"using PikeVM for is-match search at {:?}",
input.get_span()
);
let e = self.pikevm.get();
e.is_match(&mut cache.pikevm, input)
}
}

fn is_capture_search_needed(&self, slots_len: usize) -> bool {
slots_len > self.nfa.group_info().implicit_slot_len()
}
Expand Down Expand Up @@ -700,7 +732,7 @@ impl Strategy for Core {
// The main difference with 'search' is that if we're using a DFA, we
// can use a single forward scan without needing to run the reverse
// DFA.
return if let Some(e) = self.dfa.get(input) {
if let Some(e) = self.dfa.get(input) {
trace!("using full DFA for half search at {:?}", input.get_span());
match e.try_search_half_fwd(input) {
Ok(x) => x,
Expand All @@ -720,7 +752,38 @@ impl Strategy for Core {
}
} else {
self.search_half_nofail(cache, input)
};
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
if let Some(e) = self.dfa.get(input) {
trace!(
"using full DFA for is-match search at {:?}",
input.get_span()
);
match e.try_search_half_fwd(input) {
Ok(x) => x.is_some(),
Err(_err) => {
trace!("full DFA half search failed: {}", _err);
self.is_match_nofail(cache, input)
}
}
} else if let Some(e) = self.hybrid.get(input) {
trace!(
"using lazy DFA for is-match search at {:?}",
input.get_span()
);
match e.try_search_half_fwd(&mut cache.hybrid, input) {
Ok(x) => x.is_some(),
Err(_err) => {
trace!("lazy DFA half search failed: {}", _err);
self.is_match_nofail(cache, input)
}
}
} else {
self.is_match_nofail(cache, input)
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
Expand Down Expand Up @@ -980,6 +1043,21 @@ impl Strategy for ReverseAnchored {
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
if input.get_anchored().is_anchored() {
return self.core.is_match(cache, input);
}
match self.try_search_half_anchored_rev(cache, input) {
Err(_err) => {
trace!("fast reverse anchored search failed: {}", _err);
self.core.is_match_nofail(cache, input)
}
Ok(None) => false,
Ok(Some(_)) => true,
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn search_slots(
&self,
Expand Down Expand Up @@ -1332,6 +1410,28 @@ impl Strategy for ReverseSuffix {
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
if input.get_anchored().is_anchored() {
return self.core.is_match(cache, input);
}
match self.try_search_half_start(cache, input) {
Err(RetryError::Quadratic(_err)) => {
trace!("reverse suffix half optimization failed: {}", _err);
self.core.is_match_nofail(cache, input)
}
Err(RetryError::Fail(_err)) => {
trace!(
"reverse suffix reverse fast half search failed: {}",
_err
);
self.core.is_match_nofail(cache, input)
}
Ok(None) => false,
Ok(Some(_)) => true,
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn search_slots(
&self,
Expand Down Expand Up @@ -1480,7 +1580,7 @@ impl ReverseInner {
.utf8(core.info.config().get_utf8_empty())
.nfa_size_limit(core.info.config().get_nfa_size_limit())
.shrink(false)
.captures(false)
.which_captures(WhichCaptures::None)
.look_matcher(lookm);
let result = thompson::Compiler::new()
.configure(thompson_config)
Expand Down Expand Up @@ -1714,6 +1814,25 @@ impl Strategy for ReverseInner {
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
if input.get_anchored().is_anchored() {
return self.core.is_match(cache, input);
}
match self.try_search_full(cache, input) {
Err(RetryError::Quadratic(_err)) => {
trace!("reverse inner half optimization failed: {}", _err);
self.core.is_match_nofail(cache, input)
}
Err(RetryError::Fail(_err)) => {
trace!("reverse inner fast half search failed: {}", _err);
self.core.is_match_nofail(cache, input)
}
Ok(None) => false,
Ok(Some(_)) => true,
}
}

#[cfg_attr(feature = "perf-inline", inline(always))]
fn search_slots(
&self,
Expand Down