Skip to content

Commit bc69b03

Browse files
author
Ethan Pailes
committed
Don't use dfa for anchored strings with captures
The DFA can't produce captures, but is still faster than the Pike VM NFA, so the normal approach to finding capture groups is to look for the entire match with the DFA and then run the NFA on the substring of the input that matched. In cases where the regex in anchored, the match always starts at the beginning of the input, so there is never any point to trying the DFA first. The DFA can still be useful for rejecting inputs which are not in the language of the regular expression, but anchored regex with capture groups are most commonly used in a parsing context, so it seems like a fair trade-off. For a more in depth discussion see github issue #348.
1 parent 57426f6 commit bc69b03

File tree

3 files changed

+126
-5
lines changed

3 files changed

+126
-5
lines changed

bench/src/bench.rs

+35
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,41 @@ macro_rules! bench_find {
236236
}
237237
}
238238

239+
// USAGE: bench_captures!(name, pattern, groups, haystack);
240+
//
241+
// CONTRACT:
242+
// Given:
243+
// ident, the desired benchmarking function name
244+
// pattern : ::Regex, the regular expression to be executed
245+
// groups : usize, the number of capture groups
246+
// haystack : String, the string to search
247+
// bench_captures will benchmark how fast re.captures() produces
248+
// the capture groups in question.
249+
macro_rules! bench_captures {
250+
($name:ident, $pattern:expr, $count:expr, $haystack:expr) => {
251+
252+
#[cfg(feature = "re-rust")]
253+
#[bench]
254+
fn $name(b: &mut Bencher) {
255+
use std::sync::Mutex;
256+
257+
lazy_static! {
258+
static ref RE: Mutex<Regex> = Mutex::new($pattern);
259+
static ref TEXT: Mutex<Text> = Mutex::new(text!($haystack));
260+
};
261+
let re = RE.lock().unwrap();
262+
let text = TEXT.lock().unwrap();
263+
b.bytes = text.len() as u64;
264+
b.iter(|| {
265+
match re.captures(&text) {
266+
None => assert!(false, "no captures"),
267+
Some(caps) => assert_eq!($count + 1, caps.len()),
268+
}
269+
});
270+
}
271+
}
272+
}
273+
239274
mod ffi;
240275
mod misc;
241276
mod regexdna;

bench/src/misc.rs

+82
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,85 @@ macro_rules! reallyhard2 { () => (r"\w+\s+Holmes") }
191191

192192
bench_match!(reallyhard2_1K, reallyhard2!(),
193193
get_text(TXT_1K, reallyhard2_suffix()));
194+
195+
196+
//
197+
// Benchmarks to justify the short-haystack NFA fallthrough optimization
198+
// implemented by `read_captures_at` in regex/src/exec.rs. See github issue
199+
// #348.
200+
//
201+
// The procedure used to try to determine the right hardcoded cutoff
202+
// for the short-haystack optimization in issue #348 is as follows.
203+
//
204+
// ```
205+
// > cd bench
206+
// > cargo bench --features re-rust short_hay | tee dfa-nfa.res
207+
// > # modify the `MatchType::Dfa` branch in exec.rs:read_captures_at
208+
// > # to just execute the nfa
209+
// > cargo bench --features re-rust short_hay | tee nfa-only.res
210+
// > cargo benchcmp dfa-nfa.res nfa-only.res
211+
// ```
212+
//
213+
// The expected result is that short inputs will go faster under
214+
// the nfa-only mode, but at some turnover point the dfa-nfa mode
215+
// will start to win again. Unfortunately, that is not what happened.
216+
// Instead there was no noticeable change in the bench results, so
217+
// I've opted to just do the more conservative anchor optimization.
218+
//
219+
bench_captures!(short_haystack_1x,
220+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
221+
String::from("aaaabbbbccccbbbdddd"));
222+
bench_captures!(short_haystack_2x,
223+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
224+
format!("{}bbbbccccbbb{}",
225+
repeat("aaaa").take(2).collect::<String>(),
226+
repeat("dddd").take(2).collect::<String>(),
227+
));
228+
bench_captures!(short_haystack_3x,
229+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
230+
format!("{}bbbbccccbbb{}",
231+
repeat("aaaa").take(3).collect::<String>(),
232+
repeat("dddd").take(3).collect::<String>(),
233+
));
234+
bench_captures!(short_haystack_4x,
235+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
236+
format!("{}bbbbccccbbb{}",
237+
repeat("aaaa").take(4).collect::<String>(),
238+
repeat("dddd").take(4).collect::<String>(),
239+
));
240+
bench_captures!(short_haystack_10x,
241+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
242+
format!("{}bbbbccccbbb{}",
243+
repeat("aaaa").take(10).collect::<String>(),
244+
repeat("dddd").take(10).collect::<String>(),
245+
));
246+
bench_captures!(short_haystack_100x,
247+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
248+
format!("{}bbbbccccbbb{}",
249+
repeat("aaaa").take(100).collect::<String>(),
250+
repeat("dddd").take(100).collect::<String>(),
251+
));
252+
bench_captures!(short_haystack_1000x,
253+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
254+
format!("{}bbbbccccbbb{}",
255+
repeat("aaaa").take(1000).collect::<String>(),
256+
repeat("dddd").take(1000).collect::<String>(),
257+
));
258+
bench_captures!(short_haystack_10000x,
259+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
260+
format!("{}bbbbccccbbb{}",
261+
repeat("aaaa").take(10000).collect::<String>(),
262+
repeat("dddd").take(10000).collect::<String>(),
263+
));
264+
bench_captures!(short_haystack_100000x,
265+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
266+
format!("{}bbbbccccbbb{}",
267+
repeat("aaaa").take(100000).collect::<String>(),
268+
repeat("dddd").take(100000).collect::<String>(),
269+
));
270+
bench_captures!(short_haystack_1000000x,
271+
Regex::new(r"(bbbb)cccc(bbb)").unwrap(), 2,
272+
format!("{}bbbbccccbbb{}",
273+
repeat("aaaa").take(1000000).collect::<String>(),
274+
repeat("dddd").take(1000000).collect::<String>(),
275+
));

src/exec.rs

+9-5
Original file line numberDiff line numberDiff line change
@@ -554,12 +554,16 @@ impl<'c> RegularExpression for ExecNoSync<'c> {
554554
})
555555
}
556556
MatchType::Dfa => {
557-
match self.find_dfa_forward(text, start) {
558-
dfa::Result::Match((s, e)) => {
559-
self.captures_nfa_with_match(slots, text, s, e)
557+
if self.ro.nfa.is_anchored_start {
558+
self.captures_nfa(slots, text, start)
559+
} else {
560+
match self.find_dfa_forward(text, start) {
561+
dfa::Result::Match((s, e)) => {
562+
self.captures_nfa_with_match(slots, text, s, e)
563+
}
564+
dfa::Result::NoMatch(_) => None,
565+
dfa::Result::Quit => self.captures_nfa(slots, text, start),
560566
}
561-
dfa::Result::NoMatch(_) => None,
562-
dfa::Result::Quit => self.captures_nfa(slots, text, start),
563567
}
564568
}
565569
MatchType::DfaAnchoredReverse => {

0 commit comments

Comments
 (0)