Skip to content

Commit 2103045

Browse files
Ethan PailesBurntSushi
Ethan Pailes
authored andcommitted
search: skip dfa for anchored pats 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. Fixes #348
1 parent 99fd316 commit 2103045

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
@@ -225,6 +225,41 @@ macro_rules! bench_find {
225225
}
226226
}
227227

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

bench/src/misc.rs

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

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

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)