Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ harness = false
name = "matching_find"
harness = false

[[bench]]
name = "matching_matches_at"
harness = false

[[bench]]
name = "matching_starts_with"
harness = false
Expand Down
16 changes: 8 additions & 8 deletions benches/bit_ops_shl_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,45 @@ fn main() {
}

// Shift by 1 bit (bit-level shift, no word-level shortcut).
#[divan::bench(name = "shl/len_4096_by_1/owned")]
#[divan::bench(name = "shl_assign/len_4096_by_1/owned")]
fn shl_len_4096_by_1_owned(bencher: Bencher) {
bench_shl(bencher, 4096, 1);
}

#[divan::bench(name = "shl/len_4096_by_1/assign")]
#[divan::bench(name = "shl_assign/len_4096_by_1/assign")]
fn shl_len_4096_by_1_assign(bencher: Bencher) {
bench_shl_assign(bencher, 4096, 1);
}

// Shift by 64 bits (word-level shift, the fast path).
#[divan::bench(name = "shl/len_4096_by_64/owned")]
#[divan::bench(name = "shl_assign/len_4096_by_64/owned")]
fn shl_len_4096_by_64_owned(bencher: Bencher) {
bench_shl(bencher, 4096, 64);
}

#[divan::bench(name = "shl/len_4096_by_64/assign")]
#[divan::bench(name = "shl_assign/len_4096_by_64/assign")]
fn shl_len_4096_by_64_assign(bencher: Bencher) {
bench_shl_assign(bencher, 4096, 64);
}

// Shift large array by 1 — worst-case for SIMD (cascading carries).
#[divan::bench(name = "shl/len_65536_by_1/owned")]
#[divan::bench(name = "shl_assign/len_65536_by_1/owned")]
fn shl_len_65536_by_1_owned(bencher: Bencher) {
bench_shl(bencher, 65_536, 1);
}

#[divan::bench(name = "shl/len_65536_by_1/assign")]
#[divan::bench(name = "shl_assign/len_65536_by_1/assign")]
fn shl_len_65536_by_1_assign(bencher: Bencher) {
bench_shl_assign(bencher, 65_536, 1);
}

// Shift by a mixed amount (both word and bit shift components).
#[divan::bench(name = "shl/len_65536_by_17/owned")]
#[divan::bench(name = "shl_assign/len_65536_by_17/owned")]
fn shl_len_65536_by_17_owned(bencher: Bencher) {
bench_shl(bencher, 65_536, 17);
}

#[divan::bench(name = "shl/len_65536_by_17/assign")]
#[divan::bench(name = "shl_assign/len_65536_by_17/assign")]
fn shl_len_65536_by_17_assign(bencher: Bencher) {
bench_shl_assign(bencher, 65_536, 17);
}
Expand Down
144 changes: 144 additions & 0 deletions benches/matching_matches_at.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use bit_string::BitString;
use divan::{Bencher, black_box};
use int_interval::UsizeCO;

fn main() {
divan::main();
}

struct Case {
haystack_bits: BitString,
pattern_bits: BitString,
haystack_string: String,
pattern_string: String,
index: usize,
}

fn make_bits(len: usize) -> BitString {
let mut bits = BitString::zeros(len);
for i in 0..len {
if (i as u64 * 17 + 3) % 7 == 0 {
bits.set(i, true);
}
}
bits
}

fn iv(start: usize, len: usize) -> UsizeCO {
UsizeCO::checked_from_start_len(start, len).unwrap()
}

fn make_case(len: usize, pat_len: usize, index: usize) -> Case {
let h = make_bits(len);
let p = h.slice(iv(index, pat_len));
Case {
haystack_string: h.to_string(),
pattern_string: p.to_string(),
haystack_bits: h,
pattern_bits: p,
index,
}
}

fn no_case(len: usize, pat_len: usize, index: usize) -> Case {
let h = make_bits(len);
let mut p = h.slice(iv(index, pat_len));
p.set(0, !p.get(0).unwrap());
Case {
haystack_string: h.to_string(),
pattern_string: p.to_string(),
haystack_bits: h,
pattern_bits: p,
index,
}
}

// ---------------------------------------------------------------------------
// 65-bit haystack (small — scalar path)
// ---------------------------------------------------------------------------

#[divan::bench(name = "matches_at/len_65/yes/aligned/bit_string")]
fn m65ya(b: Bencher) {
b_bit(b, make_case(65, 4, 64));
}
#[divan::bench(name = "matches_at/len_65/yes/aligned/string")]
fn m65yas(b: Bencher) {
b_str(b, make_case(65, 4, 64));
}
#[divan::bench(name = "matches_at/len_65/yes/unaligned/bit_string")]
fn m65yu(b: Bencher) {
b_bit(b, make_case(65, 4, 3));
}
#[divan::bench(name = "matches_at/len_65/yes/unaligned/string")]
fn m65yus(b: Bencher) {
b_str(b, make_case(65, 4, 3));
}
#[divan::bench(name = "matches_at/len_65/no/aligned/bit_string")]
fn m65na(b: Bencher) {
b_bit(b, no_case(65, 4, 64));
}
#[divan::bench(name = "matches_at/len_65/no/aligned/string")]
fn m65nas(b: Bencher) {
b_str(b, no_case(65, 4, 64));
}
#[divan::bench(name = "matches_at/len_65/no/unaligned/bit_string")]
fn m65nu(b: Bencher) {
b_bit(b, no_case(65, 4, 3));
}
#[divan::bench(name = "matches_at/len_65/no/unaligned/string")]
fn m65nus(b: Bencher) {
b_str(b, no_case(65, 4, 3));
}

// ---------------------------------------------------------------------------
// 65536-bit haystack (large — SIMD path)
// ---------------------------------------------------------------------------

#[divan::bench(name = "matches_at/len_65536/yes/aligned/bit_string")]
fn m6ya(b: Bencher) {
b_bit(b, make_case(65_536, 128, 64));
}
#[divan::bench(name = "matches_at/len_65536/yes/aligned/string")]
fn m6yas(b: Bencher) {
b_str(b, make_case(65_536, 128, 64));
}
#[divan::bench(name = "matches_at/len_65536/yes/unaligned/bit_string")]
fn m6yu(b: Bencher) {
b_bit(b, make_case(65_536, 128, 3));
}
#[divan::bench(name = "matches_at/len_65536/yes/unaligned/string")]
fn m6yus(b: Bencher) {
b_str(b, make_case(65_536, 128, 3));
}
#[divan::bench(name = "matches_at/len_65536/no/aligned/bit_string")]
fn m6na(b: Bencher) {
b_bit(b, no_case(65_536, 128, 64));
}
#[divan::bench(name = "matches_at/len_65536/no/aligned/string")]
fn m6nas(b: Bencher) {
b_str(b, no_case(65_536, 128, 64));
}
#[divan::bench(name = "matches_at/len_65536/no/unaligned/bit_string")]
fn m6nu(b: Bencher) {
b_bit(b, no_case(65_536, 128, 3));
}
#[divan::bench(name = "matches_at/len_65536/no/unaligned/string")]
fn m6nus(b: Bencher) {
b_str(b, no_case(65_536, 128, 3));
}

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

fn b_bit(b: Bencher, c: Case) {
b.bench(|| black_box(&c.haystack_bits).matches_at(c.index, black_box(&c.pattern_bits)));
}

fn b_str(b: Bencher, c: Case) {
b.bench(|| {
let h = black_box(&c.haystack_string);
let p = black_box(&c.pattern_string);
h.as_bytes()[c.index..].starts_with(p.as_bytes())
});
}
31 changes: 0 additions & 31 deletions src/bit_string/impls_for_matching.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,7 @@
use crate::bit_string::traits::*;
use crate::funcs_for_bits::*;

use super::*;

/// Compare `needle` bits against `haystack` starting at `offset`, using
/// word-level reads so that each iteration compares up to 64 bits.
#[inline]
fn bits_equal_at(haystack: &BitString, offset: usize, needle: &BitString) -> bool {
let needle_bits = needle.bit_len;
let needle_words = needle.as_words();
let full_words = needle_bits / WORD_BITS;
let rem_bits = needle_bits % WORD_BITS;

// Full u64 words — needle is always word-aligned at index 0 so we
// compare needle_words[i] directly.
for i in 0..full_words {
let h = haystack.words.read_word_at(offset + i * WORD_BITS);
if h != needle_words[i] {
return false;
}
}

// Last partial word (if any).
if rem_bits > 0 {
let mask = low_mask(rem_bits);
let h = haystack.words.read_word_at(offset + full_words * WORD_BITS);
if (h & mask) != (needle_words[full_words] & mask) {
return false;
}
}

true
}

mod impls_for_find;
mod impls_for_matches_at;
mod impls_for_strip;
Expand Down
10 changes: 5 additions & 5 deletions src/bit_string/impls_for_matching/impls_for_find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl BitString {
self.bit_len,
needle.as_words(),
needle.bit_len,
&mut |pos| bits_equal_at(self, pos, needle),
&mut |pos| self.bits_equal_at(pos, needle),
)
.is_some()
}
Expand All @@ -39,7 +39,7 @@ impl BitString {
self.bit_len,
needle.as_words(),
needle.bit_len,
&mut |pos| bits_equal_at(self, pos, needle),
&mut |pos| self.bits_equal_at(pos, needle),
)
.is_some()
{
Expand All @@ -51,7 +51,7 @@ impl BitString {
self.bit_len,
needle.as_words(),
needle.bit_len,
&mut |pos| bits_equal_at(self, pos, needle),
&mut |pos| self.bits_equal_at(pos, needle),
)
}

Expand All @@ -68,7 +68,7 @@ impl BitString {
self.bit_len,
needle.as_words(),
needle.bit_len,
&mut |pos| bits_equal_at(self, pos, needle),
&mut |pos| self.bits_equal_at(pos, needle),
)
.is_some()
{
Expand All @@ -80,7 +80,7 @@ impl BitString {
self.bit_len,
needle.as_words(),
needle.bit_len,
&mut |pos| bits_equal_at(self, pos, needle),
&mut |pos| self.bits_equal_at(pos, needle),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ use proptest::prelude::*;

use crate::BitString;

use super::super::bits_equal_at;

fn config() -> ProptestConfig {
ProptestConfig {
cases: 512,
Expand All @@ -33,7 +31,7 @@ proptest! {
haystack.bit_len(),
needle.as_words(),
needle.bit_len(),
&mut |pos| bits_equal_at(&haystack, pos, &needle),
&mut |pos| haystack.bits_equal_at(pos, &needle),
);

// Brute-force reference: find any match.
Expand Down Expand Up @@ -77,7 +75,7 @@ proptest! {
needle.as_words(),
needle_len,
&mut |pos| {
let ok = bits_equal_at(&haystack, pos, &needle);
let ok = haystack.bits_equal_at(pos, &needle);
if ok { any_found = true; }
ok
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ mod neon {
(w0 >> shift) | (w1 << (WORD_BITS - shift))
};
}
let windows = vld1q_u64(wins.as_ptr());
let windows = unsafe { vld1q_u64(wins.as_ptr()) };
let m = vandq_u64(windows, mask);
let c = vceqq_u64(m, needle);
if vgetq_lane_u64(c, 0) != 0 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ where
let base = i * WORD_BITS;
let w0 = haystack[i];
let w1 = haystack.get(i + 1).copied().unwrap_or(0);
// Note: the SIMD backends compute max_shift differently —
// `WORD_BITS.min(last_start - base + 1)` — to process
// shifts in SIMD-sized chunks (2 or 4), relying on
// `pos <= last_start` to skip out-of-range positions.
let max_shift = (last_start - base).min(WORD_BITS - 1);
for shift in (0..=max_shift).rev() {
let pos = base + shift;
Expand Down Expand Up @@ -139,6 +143,9 @@ mod sse2 {
let w1 = haystack.get(i + 1).copied().unwrap_or(0);
let max_shift = WORD_BITS.min(last_start - base + 1);

// Round up to a multiple of 2 so the SIMD loop
// processes shifts in 2-lane pairs. Out-of-range
// positions are guarded by `pos <= last_start`.
let mut s = max_shift.next_multiple_of(2).min(WORD_BITS);
while s > 0 {
s -= 2;
Expand Down Expand Up @@ -219,6 +226,9 @@ mod avx2 {
let w1 = haystack.get(i + 1).copied().unwrap_or(0);
let max_shift = WORD_BITS.min(last_start - base + 1);

// Round up to a multiple of 4 so the SIMD loop
// processes shifts in 4-lane groups. Out-of-range
// positions are guarded by `pos <= last_start`.
let mut s = max_shift.next_multiple_of(4).min(WORD_BITS);
while s > 0 {
s -= 4;
Expand Down Expand Up @@ -291,6 +301,9 @@ mod neon {
let w1 = haystack.get(i + 1).copied().unwrap_or(0);
let max_shift = WORD_BITS.min(last_start - base + 1);

// Round up to a multiple of 2 so the SIMD loop
// processes shifts in 2-lane pairs. Out-of-range
// positions are guarded by `pos <= last_start`.
let mut s = max_shift.next_multiple_of(2).min(WORD_BITS);
while s > 0 {
s -= 2;
Expand All @@ -305,7 +318,7 @@ mod neon {
(w0 >> shift) | (w1 << (WORD_BITS - shift))
};
}
let windows = vld1q_u64(wins.as_ptr());
let windows = unsafe { vld1q_u64(wins.as_ptr()) };
let m = vandq_u64(windows, mask);
let c = vceqq_u64(m, needle);

Expand Down
Loading
Loading