From 00132307c32629ce5d50412cb065331815639c38 Mon Sep 17 00:00:00 2001 From: fereidani Date: Mon, 3 Aug 2026 02:00:12 +0330 Subject: [PATCH 1/2] Add ByteNeedle search in StrSearcherImpl --- library/core/src/str/pattern.rs | 96 +++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/library/core/src/str/pattern.rs b/library/core/src/str/pattern.rs index c014bc2818b0e..aaedaf616135a 100644 --- a/library/core/src/str/pattern.rs +++ b/library/core/src/str/pattern.rs @@ -1071,6 +1071,7 @@ pub struct StrSearcher<'a, 'b> { #[derive(Clone, Debug)] enum StrSearcherImpl { Empty(EmptyNeedle), + Byte(ByteNeedle), TwoWay(TwoWaySearcher), } @@ -1084,6 +1085,16 @@ struct EmptyNeedle { is_finished: bool, } +/// Fast searcher for a single-byte needle using `memchr`/`memrchr`. +#[derive(Clone, Debug)] +struct ByteNeedle { + b: u8, + /// Forward cursor: `haystack[..position]` has already been reported. + position: usize, + /// Backward cursor: `haystack[end..]` has already been reported. + end: usize, +} + impl<'a, 'b> StrSearcher<'a, 'b> { fn new(haystack: &'a str, needle: &'b str) -> StrSearcher<'a, 'b> { if needle.is_empty() { @@ -1098,6 +1109,12 @@ impl<'a, 'b> StrSearcher<'a, 'b> { is_finished: false, }), } + } else if let &[b] = needle.as_bytes() { + StrSearcher { + haystack, + needle, + searcher: StrSearcherImpl::Byte(ByteNeedle { b, position: 0, end: haystack.len() }), + } } else { StrSearcher { haystack, @@ -1140,6 +1157,23 @@ unsafe impl<'a, 'b> Searcher<'a> for StrSearcher<'a, 'b> { } } } + StrSearcherImpl::Byte(ref mut searcher) => { + let bytes = self.haystack.as_bytes(); + let pos = searcher.position; + if pos >= bytes.len() { + return SearchStep::Done; + } + if bytes[pos] == searcher.b { + searcher.position = pos + 1; + SearchStep::Match(pos, pos + 1) + } else { + // `pos` is always on a char boundary, so this rejects + // exactly the char starting at `pos`. + let end = self.haystack.ceil_char_boundary(pos + 1); + searcher.position = end; + SearchStep::Reject(pos, end) + } + } StrSearcherImpl::TwoWay(ref mut searcher) => { // TwoWaySearcher produces valid *Match* indices that split at char boundaries // as long as it does correct matching and that haystack and needle are @@ -1155,11 +1189,9 @@ unsafe impl<'a, 'b> Searcher<'a> for StrSearcher<'a, 'b> { self.needle.as_bytes(), is_long, ) { - SearchStep::Reject(a, mut b) => { + SearchStep::Reject(a, b) => { // skip to next char boundary - while !self.haystack.is_char_boundary(b) { - b += 1; - } + let b = self.haystack.ceil_char_boundary(b); searcher.position = cmp::max(b, searcher.position); SearchStep::Reject(a, b) } @@ -1179,6 +1211,23 @@ unsafe impl<'a, 'b> Searcher<'a> for StrSearcher<'a, 'b> { SearchStep::Reject(..) => {} } }, + StrSearcherImpl::Byte(ref mut searcher) => { + let bytes = self.haystack.as_bytes(); + if searcher.position >= bytes.len() { + return None; + } + match memchr::memchr(searcher.b, &bytes[searcher.position..]) { + Some(i) => { + let pos = searcher.position + i; + searcher.position = pos + 1; + Some((pos, pos + 1)) + } + None => { + searcher.position = bytes.len(); + None + } + } + } StrSearcherImpl::TwoWay(ref mut searcher) => { let is_long = searcher.memory == usize::MAX; // write out `true` and `false` cases to encourage the compiler @@ -1224,6 +1273,21 @@ unsafe impl<'a, 'b> ReverseSearcher<'a> for StrSearcher<'a, 'b> { } } } + StrSearcherImpl::Byte(ref mut searcher) => { + let end = searcher.end; + if end == 0 { + return SearchStep::Done; + } + let bytes = self.haystack.as_bytes(); + if bytes[end - 1] == searcher.b { + searcher.end = end - 1; + SearchStep::Match(end - 1, end) + } else { + let start = self.haystack.floor_char_boundary(end - 1); + searcher.end = start; + SearchStep::Reject(start, end) + } + } StrSearcherImpl::TwoWay(ref mut searcher) => { if searcher.end == 0 { return SearchStep::Done; @@ -1234,11 +1298,9 @@ unsafe impl<'a, 'b> ReverseSearcher<'a> for StrSearcher<'a, 'b> { self.needle.as_bytes(), is_long, ) { - SearchStep::Reject(mut a, b) => { - // skip to next char boundary - while !self.haystack.is_char_boundary(a) { - a -= 1; - } + SearchStep::Reject(a, b) => { + // skip to previous char boundary + let a = self.haystack.floor_char_boundary(a); searcher.end = cmp::min(a, searcher.end); SearchStep::Reject(a, b) } @@ -1258,6 +1320,22 @@ unsafe impl<'a, 'b> ReverseSearcher<'a> for StrSearcher<'a, 'b> { SearchStep::Reject(..) => {} } }, + StrSearcherImpl::Byte(ref mut searcher) => { + if searcher.end == 0 { + return None; + } + let bytes = self.haystack.as_bytes(); + match memchr::memrchr(searcher.b, &bytes[..searcher.end]) { + Some(i) => { + searcher.end = i; + Some((i, i + 1)) + } + None => { + searcher.end = 0; + None + } + } + } StrSearcherImpl::TwoWay(ref mut searcher) => { let is_long = searcher.memory == usize::MAX; // write out `true` and `false`, like `next_match` From 54a463a364d24edefbf020e88df4e860a4358a27 Mon Sep 17 00:00:00 2001 From: fereidani Date: Mon, 3 Aug 2026 03:07:29 +0330 Subject: [PATCH 2/2] Add more pattern benchmarks --- library/coretests/benches/pattern.rs | 152 +++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/library/coretests/benches/pattern.rs b/library/coretests/benches/pattern.rs index 15dfda9cef1e5..c7bbf879f722d 100644 --- a/library/coretests/benches/pattern.rs +++ b/library/coretests/benches/pattern.rs @@ -87,3 +87,155 @@ fn rfind_str_worst_case(b: &mut Bencher) { b.bytes = haystack.len() as u64; b.iter(|| black_box(haystack.rfind("the english language"))) } + +/// 64 KiB of text that does not contain the needle. +fn haystack_without_needle() -> String { + "abcdefgh".repeat(8 * 1024) +} + +#[bench] +fn find_1byte_str_long_nomatch(b: &mut Bencher) { + let s = haystack_without_needle(); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.find(","))) +} + +#[bench] +fn find_char_long_nomatch(b: &mut Bencher) { + let s = haystack_without_needle(); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.find(','))) +} + +#[bench] +fn find_1byte_str_long_match_end(b: &mut Bencher) { + let mut s = haystack_without_needle(); + s.push(','); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.find(","))) +} + +#[bench] +fn find_char_long_match_end(b: &mut Bencher) { + let mut s = haystack_without_needle(); + s.push(','); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.find(','))) +} + +#[bench] +fn rfind_1byte_str_long_nomatch(b: &mut Bencher) { + let s = haystack_without_needle(); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.rfind(","))) +} + +#[bench] +fn rfind_char_long_nomatch(b: &mut Bencher) { + let s = haystack_without_needle(); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.rfind(','))) +} + +#[bench] +fn find_1byte_str_early_return(b: &mut Bencher) { + let mut s = String::from("abcdefg,"); + s.push_str(&haystack_without_needle()); + let haystack = black_box(s.as_str()); + b.iter(|| { + for _ in 0..1024 { + black_box(black_box(haystack).find(",")); + } + }) +} + +#[bench] +fn find_char_early_return(b: &mut Bencher) { + let mut s = String::from("abcdefg,"); + s.push_str(&haystack_without_needle()); + let haystack = black_box(s.as_str()); + b.iter(|| { + for _ in 0..1024 { + black_box(black_box(haystack).find(',')); + } + }) +} + +// Short haystacks measure searcher construction overhead as much as the scan. +#[bench] +fn find_1byte_str_short_haystack(b: &mut Bencher) { + let haystack = black_box("abcdefg,ijklmno"); + b.iter(|| { + for _ in 0..1024 { + black_box(black_box(haystack).find(",")); + } + }) +} + +#[bench] +fn find_char_short_haystack(b: &mut Bencher) { + let haystack = black_box("abcdefg,ijklmno"); + b.iter(|| { + for _ in 0..1024 { + black_box(black_box(haystack).find(',')); + } + }) +} + +// Match-dense input: a match every third byte, the worst case for any +// skip-ahead scheme since there is nothing to skip. +#[bench] +fn split_1byte_str_dense(b: &mut Bencher) { + let s = "ab,".repeat(8 * 1024); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.split(",").count())) +} + +#[bench] +fn split_char_dense(b: &mut Bencher) { + let s = "ab,".repeat(8 * 1024); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.split(',').count())) +} + +// A match every 65 bytes, resembling line splitting. +#[bench] +fn split_1byte_str_sparse(b: &mut Bencher) { + let s = format!("{},", "abcdefgh".repeat(8)).repeat(1000); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.split(",").count())) +} + +#[bench] +fn split_char_sparse(b: &mut Bencher) { + let s = format!("{},", "abcdefgh".repeat(8)).repeat(1000); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.split(',').count())) +} + +// Haystack dominated by multi-byte chars, ASCII needle. +#[bench] +fn split_1byte_str_multibyte_haystack(b: &mut Bencher) { + let s = "\u{251c}\u{2500}\u{2500} ".repeat(8 * 1024); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.split(" ").count())) +} + +#[bench] +fn split_char_multibyte_haystack(b: &mut Bencher) { + let s = "\u{251c}\u{2500}\u{2500} ".repeat(8 * 1024); + let haystack = black_box(s.as_str()); + b.bytes = haystack.len() as u64; + b.iter(|| black_box(haystack.split(' ').count())) +}