use core::fmt::Debug; use alloc::{ boxed::Box, collections::BTreeMap, format, sync::Arc, vec, vec::Vec, }; use crate::{ packed::{ ext::Pointer, pattern::Patterns, vector::{FatVector, Vector}, }, util::int::U32, PatternID, }; /// A match type specialized to the Teddy implementations below. /// /// Essentially, instead of representing a match at byte offsets, we use /// raw pointers. This is because the implementations below operate on raw /// pointers, and so this is a more natural return type based on how the /// implementation works. /// /// Also, the `PatternID` used here is a `u16`. #[derive(Clone, Copy, Debug)] pub(crate) struct Match { pid: PatternID, start: *const u8, end: *const u8, } impl Match { /// Returns the ID of the pattern that matched. pub(crate) fn pattern(&self) -> PatternID { self.pid } /// Returns a pointer into the haystack at which the match starts. pub(crate) fn start(&self) -> *const u8 { self.start } /// Returns a pointer into the haystack at which the match ends. pub(crate) fn end(&self) -> *const u8 { self.end } } /// A "slim" Teddy implementation that is generic over both the vector type /// and the minimum length of the patterns being searched for. /// /// Only 1, 2, 3 and 4 bytes are supported as minimum lengths. #[derive(Clone, Debug)] pub(crate) struct Slim { /// A generic data structure for doing "slim" Teddy verification. teddy: Teddy<8>, /// The masks used as inputs to the shuffle operation to generate /// candidates (which are fed into the verification routines). masks: [Mask; BYTES], } impl Slim { /// Create a new "slim" Teddy searcher for the given patterns. /// /// # Panics /// /// This panics when `BYTES` is any value other than 1, 2, 3 or 4. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] pub(crate) unsafe fn new(patterns: Arc) -> Slim { assert!( 1 <= BYTES && BYTES <= 4, "only 1, 2, 3 or 4 bytes are supported" ); let teddy = Teddy::new(patterns); let masks = SlimMaskBuilder::from_teddy(&teddy); Slim { teddy, masks } } /// Returns the approximate total amount of heap used by this type, in /// units of bytes. #[inline(always)] pub(crate) fn memory_usage(&self) -> usize { self.teddy.memory_usage() } /// Returns the minimum length, in bytes, that a haystack must be in order /// to use it with this searcher. #[inline(always)] pub(crate) fn minimum_len(&self) -> usize { V::BYTES + (BYTES - 1) } } impl Slim { /// Look for an occurrences of the patterns in this finder in the haystack /// given by the `start` and `end` pointers. /// /// If no match could be found, then `None` is returned. /// /// # Safety /// /// The given pointers representing the haystack must be valid to read /// from. They must also point to a region of memory that is at least the /// minimum length required by this searcher. /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start; while cur <= end.sub(V::BYTES) { if let Some(m) = self.find_one(cur, end) { return Some(m); } cur = cur.add(V::BYTES); } if cur < end { cur = end.sub(V::BYTES); if let Some(m) = self.find_one(cur, end) { return Some(m); } } None } /// Look for a match starting at the `V::BYTES` at and after `cur`. If /// there isn't one, then `None` is returned. /// /// # Safety /// /// The given pointers representing the haystack must be valid to read /// from. They must also point to a region of memory that is at least the /// minimum length required by this searcher. /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, ) -> Option { let c = self.candidate(cur); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur, end, c) { return Some(m); } } None } /// Look for a candidate match (represented as a vector) starting at the /// `V::BYTES` at and after `cur`. If there isn't one, then a vector with /// all bits set to zero is returned. /// /// # Safety /// /// The given pointer representing the haystack must be valid to read /// from. /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn candidate(&self, cur: *const u8) -> V { let chunk = V::load_unaligned(cur); Mask::members1(chunk, self.masks) } } impl Slim { /// See Slim::find. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start.add(1); let mut prev0 = V::splat(0xFF); while cur <= end.sub(V::BYTES) { if let Some(m) = self.find_one(cur, end, &mut prev0) { return Some(m); } cur = cur.add(V::BYTES); } if cur < end { cur = end.sub(V::BYTES); prev0 = V::splat(0xFF); if let Some(m) = self.find_one(cur, end, &mut prev0) { return Some(m); } } None } /// See Slim::find_one. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, prev0: &mut V, ) -> Option { let c = self.candidate(cur, prev0); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur.sub(1), end, c) { return Some(m); } } None } /// See Slim::candidate. #[inline(always)] unsafe fn candidate(&self, cur: *const u8, prev0: &mut V) -> V { let chunk = V::load_unaligned(cur); let (res0, res1) = Mask::members2(chunk, self.masks); let res0prev0 = res0.shift_in_one_byte(*prev0); let res = res0prev0.and(res1); *prev0 = res0; res } } impl Slim { /// See Slim::find. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start.add(2); let mut prev0 = V::splat(0xFF); let mut prev1 = V::splat(0xFF); while cur <= end.sub(V::BYTES) { if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1) { return Some(m); } cur = cur.add(V::BYTES); } if cur < end { cur = end.sub(V::BYTES); prev0 = V::splat(0xFF); prev1 = V::splat(0xFF); if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1) { return Some(m); } } None } /// See Slim::find_one. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, prev0: &mut V, prev1: &mut V, ) -> Option { let c = self.candidate(cur, prev0, prev1); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur.sub(2), end, c) { return Some(m); } } None } /// See Slim::candidate. #[inline(always)] unsafe fn candidate( &self, cur: *const u8, prev0: &mut V, prev1: &mut V, ) -> V { let chunk = V::load_unaligned(cur); let (res0, res1, res2) = Mask::members3(chunk, self.masks); let res0prev0 = res0.shift_in_two_bytes(*prev0); let res1prev1 = res1.shift_in_one_byte(*prev1); let res = res0prev0.and(res1prev1).and(res2); *prev0 = res0; *prev1 = res1; res } } impl Slim { /// See Slim::find. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start.add(3); let mut prev0 = V::splat(0xFF); let mut prev1 = V::splat(0xFF); let mut prev2 = V::splat(0xFF); while cur <= end.sub(V::BYTES) { if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1, &mut prev2) { return Some(m); } cur = cur.add(V::BYTES); } if cur < end { cur = end.sub(V::BYTES); prev0 = V::splat(0xFF); prev1 = V::splat(0xFF); prev2 = V::splat(0xFF); if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1, &mut prev2) { return Some(m); } } None } /// See Slim::find_one. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, prev0: &mut V, prev1: &mut V, prev2: &mut V, ) -> Option { let c = self.candidate(cur, prev0, prev1, prev2); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur.sub(3), end, c) { return Some(m); } } None } /// See Slim::candidate. #[inline(always)] unsafe fn candidate( &self, cur: *const u8, prev0: &mut V, prev1: &mut V, prev2: &mut V, ) -> V { let chunk = V::load_unaligned(cur); let (res0, res1, res2, res3) = Mask::members4(chunk, self.masks); let res0prev0 = res0.shift_in_three_bytes(*prev0); let res1prev1 = res1.shift_in_two_bytes(*prev1); let res2prev2 = res2.shift_in_one_byte(*prev2); let res = res0prev0.and(res1prev1).and(res2prev2).and(res3); *prev0 = res0; *prev1 = res1; *prev2 = res2; res } } /// A "fat" Teddy implementation that is generic over both the vector type /// and the minimum length of the patterns being searched for. /// /// Only 1, 2, 3 and 4 bytes are supported as minimum lengths. #[derive(Clone, Debug)] pub(crate) struct Fat { /// A generic data structure for doing "fat" Teddy verification. teddy: Teddy<16>, /// The masks used as inputs to the shuffle operation to generate /// candidates (which are fed into the verification routines). masks: [Mask; BYTES], } impl Fat { /// Create a new "fat" Teddy searcher for the given patterns. /// /// # Panics /// /// This panics when `BYTES` is any value other than 1, 2, 3 or 4. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] pub(crate) unsafe fn new(patterns: Arc) -> Fat { assert!( 1 <= BYTES && BYTES <= 4, "only 1, 2, 3 or 4 bytes are supported" ); let teddy = Teddy::new(patterns); let masks = FatMaskBuilder::from_teddy(&teddy); Fat { teddy, masks } } /// Returns the approximate total amount of heap used by this type, in /// units of bytes. #[inline(always)] pub(crate) fn memory_usage(&self) -> usize { self.teddy.memory_usage() } /// Returns the minimum length, in bytes, that a haystack must be in order /// to use it with this searcher. #[inline(always)] pub(crate) fn minimum_len(&self) -> usize { V::Half::BYTES + (BYTES - 1) } } impl Fat { /// Look for an occurrences of the patterns in this finder in the haystack /// given by the `start` and `end` pointers. /// /// If no match could be found, then `None` is returned. /// /// # Safety /// /// The given pointers representing the haystack must be valid to read /// from. They must also point to a region of memory that is at least the /// minimum length required by this searcher. /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start; while cur <= end.sub(V::Half::BYTES) { if let Some(m) = self.find_one(cur, end) { return Some(m); } cur = cur.add(V::Half::BYTES); } if cur < end { cur = end.sub(V::Half::BYTES); if let Some(m) = self.find_one(cur, end) { return Some(m); } } None } /// Look for a match starting at the `V::BYTES` at and after `cur`. If /// there isn't one, then `None` is returned. /// /// # Safety /// /// The given pointers representing the haystack must be valid to read /// from. They must also point to a region of memory that is at least the /// minimum length required by this searcher. /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, ) -> Option { let c = self.candidate(cur); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur, end, c) { return Some(m); } } None } /// Look for a candidate match (represented as a vector) starting at the /// `V::BYTES` at and after `cur`. If there isn't one, then a vector with /// all bits set to zero is returned. /// /// # Safety /// /// The given pointer representing the haystack must be valid to read /// from. /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn candidate(&self, cur: *const u8) -> V { let chunk = V::load_half_unaligned(cur); Mask::members1(chunk, self.masks) } } impl Fat { /// See `Fat::find`. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start.add(1); let mut prev0 = V::splat(0xFF); while cur <= end.sub(V::Half::BYTES) { if let Some(m) = self.find_one(cur, end, &mut prev0) { return Some(m); } cur = cur.add(V::Half::BYTES); } if cur < end { cur = end.sub(V::Half::BYTES); prev0 = V::splat(0xFF); if let Some(m) = self.find_one(cur, end, &mut prev0) { return Some(m); } } None } /// See `Fat::find_one`. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, prev0: &mut V, ) -> Option { let c = self.candidate(cur, prev0); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur.sub(1), end, c) { return Some(m); } } None } /// See `Fat::candidate`. #[inline(always)] unsafe fn candidate(&self, cur: *const u8, prev0: &mut V) -> V { let chunk = V::load_half_unaligned(cur); let (res0, res1) = Mask::members2(chunk, self.masks); let res0prev0 = res0.half_shift_in_one_byte(*prev0); let res = res0prev0.and(res1); *prev0 = res0; res } } impl Fat { /// See `Fat::find`. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start.add(2); let mut prev0 = V::splat(0xFF); let mut prev1 = V::splat(0xFF); while cur <= end.sub(V::Half::BYTES) { if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1) { return Some(m); } cur = cur.add(V::Half::BYTES); } if cur < end { cur = end.sub(V::Half::BYTES); prev0 = V::splat(0xFF); prev1 = V::splat(0xFF); if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1) { return Some(m); } } None } /// See `Fat::find_one`. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, prev0: &mut V, prev1: &mut V, ) -> Option { let c = self.candidate(cur, prev0, prev1); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur.sub(2), end, c) { return Some(m); } } None } /// See `Fat::candidate`. #[inline(always)] unsafe fn candidate( &self, cur: *const u8, prev0: &mut V, prev1: &mut V, ) -> V { let chunk = V::load_half_unaligned(cur); let (res0, res1, res2) = Mask::members3(chunk, self.masks); let res0prev0 = res0.half_shift_in_two_bytes(*prev0); let res1prev1 = res1.half_shift_in_one_byte(*prev1); let res = res0prev0.and(res1prev1).and(res2); *prev0 = res0; *prev1 = res1; res } } impl Fat { /// See `Fat::find`. #[inline(always)] pub(crate) unsafe fn find( &self, start: *const u8, end: *const u8, ) -> Option { let len = end.distance(start); debug_assert!(len >= self.minimum_len()); let mut cur = start.add(3); let mut prev0 = V::splat(0xFF); let mut prev1 = V::splat(0xFF); let mut prev2 = V::splat(0xFF); while cur <= end.sub(V::Half::BYTES) { if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1, &mut prev2) { return Some(m); } cur = cur.add(V::Half::BYTES); } if cur < end { cur = end.sub(V::Half::BYTES); prev0 = V::splat(0xFF); prev1 = V::splat(0xFF); prev2 = V::splat(0xFF); if let Some(m) = self.find_one(cur, end, &mut prev0, &mut prev1, &mut prev2) { return Some(m); } } None } /// See `Fat::find_one`. #[inline(always)] unsafe fn find_one( &self, cur: *const u8, end: *const u8, prev0: &mut V, prev1: &mut V, prev2: &mut V, ) -> Option { let c = self.candidate(cur, prev0, prev1, prev2); if !c.is_zero() { if let Some(m) = self.teddy.verify(cur.sub(3), end, c) { return Some(m); } } None } /// See `Fat::candidate`. #[inline(always)] unsafe fn candidate( &self, cur: *const u8, prev0: &mut V, prev1: &mut V, prev2: &mut V, ) -> V { let chunk = V::load_half_unaligned(cur); let (res0, res1, res2, res3) = Mask::members4(chunk, self.masks); let res0prev0 = res0.half_shift_in_three_bytes(*prev0); let res1prev1 = res1.half_shift_in_two_bytes(*prev1); let res2prev2 = res2.half_shift_in_one_byte(*prev2); let res = res0prev0.and(res1prev1).and(res2prev2).and(res3); *prev0 = res0; *prev1 = res1; *prev2 = res2; res } } /// The common elements of all "slim" and "fat" Teddy search implementations. /// /// Essentially, this contains the patterns and the buckets. Namely, it /// contains enough to implement the verification step after candidates are /// identified via the shuffle masks. /// /// It is generic over the number of buckets used. In general, the number of /// buckets is either 8 (for "slim" Teddy) or 16 (for "fat" Teddy). The generic /// parameter isn't really meant to be instantiated for any value other than /// 8 or 16, although it is technically possible. The main hiccup is that there /// is some bit-shifting done in the critical part of verification that could /// be quite expensive if `N` is not a multiple of 2. #[derive(Clone, Debug)] struct Teddy { /// The patterns we are searching for. /// /// A pattern string can be found by its `PatternID`. patterns: Arc, /// The allocation of patterns in buckets. This only contains the IDs of /// patterns. In order to do full verification, callers must provide the /// actual patterns when using Teddy. buckets: [Vec; BUCKETS], // N.B. The above representation is very simple, but it definitely results // in ping-ponging between different allocations during verification. I've // tried experimenting with other representations that flatten the pattern // strings into a single allocation, but it doesn't seem to help much. // Probably everything is small enough to fit into cache anyway, and so the // pointer chasing isn't a big deal? // // One other avenue I haven't explored is some kind of hashing trick // that let's us do another high-confidence check before launching into // `memcmp`. } impl Teddy { /// Create a new generic data structure for Teddy verification. fn new(patterns: Arc) -> Teddy { assert_ne!(0, patterns.len(), "Teddy requires at least one pattern"); assert_ne!( 0, patterns.minimum_len(), "Teddy does not support zero-length patterns" ); assert!( BUCKETS == 8 || BUCKETS == 16, "Teddy only supports 8 or 16 buckets" ); // MSRV(1.63): Use core::array::from_fn below instead of allocating a // superfluous outer Vec. Not a big deal (especially given the BTreeMap // allocation below), but nice to not do it. let buckets = <[Vec; BUCKETS]>::try_from(vec![vec![]; BUCKETS]) .unwrap(); let mut t = Teddy { patterns, buckets }; let mut map: BTreeMap, usize> = BTreeMap::new(); for (id, pattern) in t.patterns.iter() { // We try to be slightly clever in how we assign patterns into // buckets. Generally speaking, we want patterns with the same // prefix to be in the same bucket, since it minimizes the amount // of time we spend churning through buckets in the verification // step. // // So we could assign patterns with the same N-prefix (where N is // the size of the mask, which is one of {1, 2, 3}) to the same // bucket. However, case insensitive searches are fairly common, so // we'd for example, ideally want to treat `abc` and `ABC` as if // they shared the same prefix. ASCII has the nice property that // the lower 4 bits of A and a are the same, so we therefore group // patterns with the same low-nybble-N-prefix into the same bucket. // // MOREOVER, this is actually necessary for correctness! In // particular, by grouping patterns with the same prefix into the // same bucket, we ensure that we preserve correct leftmost-first // and leftmost-longest match semantics. In addition to the fact // that `patterns.iter()` iterates in the correct order, this // guarantees that all possible ambiguous matches will occur in // the same bucket. The verification routine could be adjusted to // support correct leftmost match semantics regardless of bucket // allocation, but that results in a performance hit. It's much // nicer to be able to just stop as soon as a match is found. let lonybs = pattern.low_nybbles(t.mask_len()); if let Some(&bucket) = map.get(&lonybs) { t.buckets[bucket].push(id); } else { // N.B. We assign buckets in reverse because it shouldn't have // any influence on performance, but it does make it harder to // get leftmost match semantics accidentally correct. let bucket = (BUCKETS - 1) - (id.as_usize() % BUCKETS); t.buckets[bucket].push(id); map.insert(lonybs, bucket); } } t } /// Verify whether there are any matches starting at or after `cur` in the /// haystack. The candidate chunk given should correspond to 8-bit bitsets /// for N buckets. /// /// # Safety /// /// The given pointers representing the haystack must be valid to read /// from. #[inline(always)] unsafe fn verify64( &self, cur: *const u8, end: *const u8, mut candidate_chunk: u64, ) -> Option { while candidate_chunk != 0 { let bit = candidate_chunk.trailing_zeros().as_usize(); candidate_chunk &= !(1 << bit); let cur = cur.add(bit / BUCKETS); let bucket = bit % BUCKETS; if let Some(m) = self.verify_bucket(cur, end, bucket) { return Some(m); } } None } /// Verify whether there are any matches starting at `at` in the given /// `haystack` corresponding only to patterns in the given bucket. /// /// # Safety /// /// The given pointers representing the haystack must be valid to read /// from. /// /// The bucket index must be less than or equal to `self.buckets.len()`. #[inline(always)] unsafe fn verify_bucket( &self, cur: *const u8, end: *const u8, bucket: usize, ) -> Option { debug_assert!(bucket < self.buckets.len()); // SAFETY: The caller must ensure that the bucket index is correct. for pid in self.buckets.get_unchecked(bucket).iter().copied() { // SAFETY: This is safe because we are guaranteed that every // index in a Teddy bucket is a valid index into `pats`, by // construction. debug_assert!(pid.as_usize() < self.patterns.len()); let pat = self.patterns.get_unchecked(pid); if pat.is_prefix_raw(cur, end) { let start = cur; let end = start.add(pat.len()); return Some(Match { pid, start, end }); } } None } /// Returns the total number of masks required by the patterns in this /// Teddy searcher. /// /// Basically, the mask length corresponds to the type of Teddy searcher /// to use: a 1-byte, 2-byte, 3-byte or 4-byte searcher. The bigger the /// better, typically, since searching for longer substrings usually /// decreases the rate of false positives. Therefore, the number of masks /// needed is the length of the shortest pattern in this searcher. If the /// length of the shortest pattern (in bytes) is bigger than 4, then the /// mask length is 4 since there are no Teddy searchers for more than 4 /// bytes. fn mask_len(&self) -> usize { core::cmp::min(4, self.patterns.minimum_len()) } /// Returns the approximate total amount of heap used by this type, in /// units of bytes. fn memory_usage(&self) -> usize { // This is an upper bound rather than a precise accounting. No // particular reason, other than it's probably very close to actual // memory usage in practice. self.patterns.len() * core::mem::size_of::() } } impl Teddy<8> { /// Runs the verification routine for "slim" Teddy. /// /// The candidate given should be a collection of 8-bit bitsets (one bitset /// per lane), where the ith bit is set in the jth lane if and only if the /// byte occurring at `at + j` in `cur` is in the bucket `i`. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. /// /// The given pointers must be valid to read from. #[inline(always)] unsafe fn verify( &self, mut cur: *const u8, end: *const u8, candidate: V, ) -> Option { debug_assert!(!candidate.is_zero()); // Convert the candidate into 64-bit chunks, and then verify each of // those chunks. candidate.for_each_64bit_lane( #[inline(always)] |_, chunk| { let result = self.verify64(cur, end, chunk); cur = cur.add(8); result }, ) } } impl Teddy<16> { /// Runs the verification routine for "fat" Teddy. /// /// The candidate given should be a collection of 8-bit bitsets (one bitset /// per lane), where the ith bit is set in the jth lane if and only if the /// byte occurring at `at + (j < 16 ? j : j - 16)` in `cur` is in the /// bucket `j < 16 ? i : i + 8`. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. /// /// The given pointers must be valid to read from. #[inline(always)] unsafe fn verify( &self, mut cur: *const u8, end: *const u8, candidate: V, ) -> Option { // This is a bit tricky, but we basically want to convert our // candidate, which looks like this (assuming a 256-bit vector): // // a31 a30 ... a17 a16 a15 a14 ... a01 a00 // // where each a(i) is an 8-bit bitset corresponding to the activated // buckets, to this // // a31 a15 a30 a14 a29 a13 ... a18 a02 a17 a01 a16 a00 // // Namely, for Fat Teddy, the high 128-bits of the candidate correspond // to the same bytes in the haystack in the low 128-bits (so we only // scan 16 bytes at a time), but are for buckets 8-15 instead of 0-7. // // The verification routine wants to look at all potentially matching // buckets before moving on to the next lane. So for example, both // a16 and a00 both correspond to the first byte in our window; a00 // contains buckets 0-7 and a16 contains buckets 8-15. Specifically, // a16 should be checked before a01. So the transformation shown above // allows us to use our normal verification procedure with one small // change: we treat each bitset as 16 bits instead of 8 bits. debug_assert!(!candidate.is_zero()); // Swap the 128-bit lanes in the candidate vector. let swapped = candidate.swap_halves(); // Interleave the bytes from the low 128-bit lanes, starting with // cand first. let r1 = candidate.interleave_low_8bit_lanes(swapped); // Interleave the bytes from the high 128-bit lanes, starting with // cand first. let r2 = candidate.interleave_high_8bit_lanes(swapped); // Now just take the 2 low 64-bit integers from both r1 and r2. We // can drop the high 64-bit integers because they are a mirror image // of the low 64-bit integers. All we care about are the low 128-bit // lanes of r1 and r2. Combined, they contain all our 16-bit bitsets // laid out in the desired order, as described above. r1.for_each_low_64bit_lane( r2, #[inline(always)] |_, chunk| { let result = self.verify64(cur, end, chunk); cur = cur.add(4); result }, ) } } /// A vector generic mask for the low and high nybbles in a set of patterns. /// Each 8-bit lane `j` in a vector corresponds to a bitset where the `i`th bit /// is set if and only if the nybble `j` is in the bucket `i` at a particular /// position. /// /// This is slightly tweaked dependending on whether Slim or Fat Teddy is being /// used. For Slim Teddy, the bitsets in the lower half are the same as the /// bitsets in the higher half, so that we can search `V::BYTES` bytes at a /// time. (Remember, the nybbles in the haystack are used as indices into these /// masks, and 256-bit shuffles only operate on 128-bit lanes.) /// /// For Fat Teddy, the bitsets are not repeated, but instead, the high half /// bits correspond to an addition 8 buckets. So that a bitset `00100010` has /// buckets 1 and 5 set if it's in the lower half, but has buckets 9 and 13 set /// if it's in the higher half. #[derive(Clone, Copy, Debug)] struct Mask { lo: V, hi: V, } impl Mask { /// Return a candidate for Teddy (fat or slim) that is searching for 1-byte /// candidates. /// /// If a candidate is returned, it will be a collection of 8-bit bitsets /// (one bitset per lane), where the ith bit is set in the jth lane if and /// only if the byte occurring at the jth lane in `chunk` is in the bucket /// `i`. If no candidate is found, then the vector returned will have all /// lanes set to zero. /// /// `chunk` should correspond to a `V::BYTES` window of the haystack (where /// the least significant byte corresponds to the start of the window). For /// fat Teddy, the haystack window length should be `V::BYTES / 2`, with /// the window repeated in each half of the vector. /// /// `mask1` should correspond to a low/high mask for the first byte of all /// patterns that are being searched. #[inline(always)] unsafe fn members1(chunk: V, masks: [Mask; 1]) -> V { let lomask = V::splat(0xF); let hlo = chunk.and(lomask); let hhi = chunk.shift_8bit_lane_right::<4>().and(lomask); let locand = masks[0].lo.shuffle_bytes(hlo); let hicand = masks[0].hi.shuffle_bytes(hhi); locand.and(hicand) } /// Return a candidate for Teddy (fat or slim) that is searching for 2-byte /// candidates. /// /// If candidates are returned, each will be a collection of 8-bit bitsets /// (one bitset per lane), where the ith bit is set in the jth lane if and /// only if the byte occurring at the jth lane in `chunk` is in the bucket /// `i`. Each candidate returned corresponds to the first and second bytes /// of the patterns being searched. If no candidate is found, then all of /// the lanes will be set to zero in at least one of the vectors returned. /// /// `chunk` should correspond to a `V::BYTES` window of the haystack (where /// the least significant byte corresponds to the start of the window). For /// fat Teddy, the haystack window length should be `V::BYTES / 2`, with /// the window repeated in each half of the vector. /// /// The masks should correspond to the masks computed for the first and /// second bytes of all patterns that are being searched. #[inline(always)] unsafe fn members2(chunk: V, masks: [Mask; 2]) -> (V, V) { let lomask = V::splat(0xF); let hlo = chunk.and(lomask); let hhi = chunk.shift_8bit_lane_right::<4>().and(lomask); let locand1 = masks[0].lo.shuffle_bytes(hlo); let hicand1 = masks[0].hi.shuffle_bytes(hhi); let cand1 = locand1.and(hicand1); let locand2 = masks[1].lo.shuffle_bytes(hlo); let hicand2 = masks[1].hi.shuffle_bytes(hhi); let cand2 = locand2.and(hicand2); (cand1, cand2) } /// Return a candidate for Teddy (fat or slim) that is searching for 3-byte /// candidates. /// /// If candidates are returned, each will be a collection of 8-bit bitsets /// (one bitset per lane), where the ith bit is set in the jth lane if and /// only if the byte occurring at the jth lane in `chunk` is in the bucket /// `i`. Each candidate returned corresponds to the first, second and third /// bytes of the patterns being searched. If no candidate is found, then /// all of the lanes will be set to zero in at least one of the vectors /// returned. /// /// `chunk` should correspond to a `V::BYTES` window of the haystack (where /// the least significant byte corresponds to the start of the window). For /// fat Teddy, the haystack window length should be `V::BYTES / 2`, with /// the window repeated in each half of the vector. /// /// The masks should correspond to the masks computed for the first, second /// and third bytes of all patterns that are being searched. #[inline(always)] unsafe fn members3(chunk: V, masks: [Mask; 3]) -> (V, V, V) { let lomask = V::splat(0xF); let hlo = chunk.and(lomask); let hhi = chunk.shift_8bit_lane_right::<4>().and(lomask); let locand1 = masks[0].lo.shuffle_bytes(hlo); let hicand1 = masks[0].hi.shuffle_bytes(hhi); let cand1 = locand1.and(hicand1); let locand2 = masks[1].lo.shuffle_bytes(hlo); let hicand2 = masks[1].hi.shuffle_bytes(hhi); let cand2 = locand2.and(hicand2); let locand3 = masks[2].lo.shuffle_bytes(hlo); let hicand3 = masks[2].hi.shuffle_bytes(hhi); let cand3 = locand3.and(hicand3); (cand1, cand2, cand3) } /// Return a candidate for Teddy (fat or slim) that is searching for 4-byte /// candidates. /// /// If candidates are returned, each will be a collection of 8-bit bitsets /// (one bitset per lane), where the ith bit is set in the jth lane if and /// only if the byte occurring at the jth lane in `chunk` is in the bucket /// `i`. Each candidate returned corresponds to the first, second, third /// and fourth bytes of the patterns being searched. If no candidate is /// found, then all of the lanes will be set to zero in at least one of the /// vectors returned. /// /// `chunk` should correspond to a `V::BYTES` window of the haystack (where /// the least significant byte corresponds to the start of the window). For /// fat Teddy, the haystack window length should be `V::BYTES / 2`, with /// the window repeated in each half of the vector. /// /// The masks should correspond to the masks computed for the first, /// second, third and fourth bytes of all patterns that are being searched. #[inline(always)] unsafe fn members4(chunk: V, masks: [Mask; 4]) -> (V, V, V, V) { let lomask = V::splat(0xF); let hlo = chunk.and(lomask); let hhi = chunk.shift_8bit_lane_right::<4>().and(lomask); let locand1 = masks[0].lo.shuffle_bytes(hlo); let hicand1 = masks[0].hi.shuffle_bytes(hhi); let cand1 = locand1.and(hicand1); let locand2 = masks[1].lo.shuffle_bytes(hlo); let hicand2 = masks[1].hi.shuffle_bytes(hhi); let cand2 = locand2.and(hicand2); let locand3 = masks[2].lo.shuffle_bytes(hlo); let hicand3 = masks[2].hi.shuffle_bytes(hhi); let cand3 = locand3.and(hicand3); let locand4 = masks[3].lo.shuffle_bytes(hlo); let hicand4 = masks[3].hi.shuffle_bytes(hhi); let cand4 = locand4.and(hicand4); (cand1, cand2, cand3, cand4) } } /// Represents the low and high nybble masks that will be used during /// search. Each mask is 32 bytes wide, although only the first 16 bytes are /// used for 128-bit vectors. /// /// Each byte in the mask corresponds to a 8-bit bitset, where bit `i` is set /// if and only if the corresponding nybble is in the ith bucket. The index of /// the byte (0-15, inclusive) corresponds to the nybble. /// /// Each mask is used as the target of a shuffle, where the indices for the /// shuffle are taken from the haystack. AND'ing the shuffles for both the /// low and high masks together also results in 8-bit bitsets, but where bit /// `i` is set if and only if the correspond *byte* is in the ith bucket. #[derive(Clone, Default)] struct SlimMaskBuilder { lo: [u8; 32], hi: [u8; 32], } impl SlimMaskBuilder { /// Update this mask by adding the given byte to the given bucket. The /// given bucket must be in the range 0-7. /// /// # Panics /// /// When `bucket >= 8`. fn add(&mut self, bucket: usize, byte: u8) { assert!(bucket < 8); let bucket = u8::try_from(bucket).unwrap(); let byte_lo = usize::from(byte & 0xF); let byte_hi = usize::from((byte >> 4) & 0xF); // When using 256-bit vectors, we need to set this bucket assignment in // the low and high 128-bit portions of the mask. This allows us to // process 32 bytes at a time. Namely, AVX2 shuffles operate on each // of the 128-bit lanes, rather than the full 256-bit vector at once. self.lo[byte_lo] |= 1 << bucket; self.lo[byte_lo + 16] |= 1 << bucket; self.hi[byte_hi] |= 1 << bucket; self.hi[byte_hi + 16] |= 1 << bucket; } /// Turn this builder into a vector mask. /// /// # Panics /// /// When `V` represents a vector bigger than what `MaskBytes` can contain. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn build(&self) -> Mask { assert!(V::BYTES <= self.lo.len()); assert!(V::BYTES <= self.hi.len()); Mask { lo: V::load_unaligned(self.lo[..].as_ptr()), hi: V::load_unaligned(self.hi[..].as_ptr()), } } /// A convenience function for building `N` vector masks from a slim /// `Teddy` value. /// /// # Panics /// /// When `V` represents a vector bigger than what `MaskBytes` can contain. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn from_teddy( teddy: &Teddy<8>, ) -> [Mask; BYTES] { // MSRV(1.63): Use core::array::from_fn to just build the array here // instead of creating a vector and turning it into an array. let mut mask_builders = vec![SlimMaskBuilder::default(); BYTES]; for (bucket_index, bucket) in teddy.buckets.iter().enumerate() { for pid in bucket.iter().copied() { let pat = teddy.patterns.get(pid); for (i, builder) in mask_builders.iter_mut().enumerate() { builder.add(bucket_index, pat.bytes()[i]); } } } let array = <[SlimMaskBuilder; BYTES]>::try_from(mask_builders).unwrap(); array.map(|builder| builder.build()) } } impl Debug for SlimMaskBuilder { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let (mut parts_lo, mut parts_hi) = (vec![], vec![]); for i in 0..32 { parts_lo.push(format!("{:02}: {:08b}", i, self.lo[i])); parts_hi.push(format!("{:02}: {:08b}", i, self.hi[i])); } f.debug_struct("SlimMaskBuilder") .field("lo", &parts_lo) .field("hi", &parts_hi) .finish() } } /// Represents the low and high nybble masks that will be used during "fat" /// Teddy search. /// /// Each mask is 32 bytes wide, and at the time of writing, only 256-bit vectors /// support fat Teddy. /// /// A fat Teddy mask is like a slim Teddy mask, except that instead of /// repeating the bitsets in the high and low 128-bits in 256-bit vectors, the /// high and low 128-bit halves each represent distinct buckets. (Bringing the /// total to 16 instead of 8.) This permits spreading the patterns out a bit /// more and thus putting less pressure on verification to be fast. /// /// Each byte in the mask corresponds to a 8-bit bitset, where bit `i` is set /// if and only if the corresponding nybble is in the ith bucket. The index of /// the byte (0-15, inclusive) corresponds to the nybble. #[derive(Clone, Copy, Default)] struct FatMaskBuilder { lo: [u8; 32], hi: [u8; 32], } impl FatMaskBuilder { /// Update this mask by adding the given byte to the given bucket. The /// given bucket must be in the range 0-15. /// /// # Panics /// /// When `bucket >= 16`. fn add(&mut self, bucket: usize, byte: u8) { assert!(bucket < 16); let bucket = u8::try_from(bucket).unwrap(); let byte_lo = usize::from(byte & 0xF); let byte_hi = usize::from((byte >> 4) & 0xF); // Unlike slim teddy, fat teddy only works with AVX2. For fat teddy, // the high 128 bits of our mask correspond to buckets 8-15, while the // low 128 bits correspond to buckets 0-7. if bucket < 8 { self.lo[byte_lo] |= 1 << bucket; self.hi[byte_hi] |= 1 << bucket; } else { self.lo[byte_lo + 16] |= 1 << (bucket % 8); self.hi[byte_hi + 16] |= 1 << (bucket % 8); } } /// Turn this builder into a vector mask. /// /// # Panics /// /// When `V` represents a vector bigger than what `MaskBytes` can contain. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn build(&self) -> Mask { assert!(V::BYTES <= self.lo.len()); assert!(V::BYTES <= self.hi.len()); Mask { lo: V::load_unaligned(self.lo[..].as_ptr()), hi: V::load_unaligned(self.hi[..].as_ptr()), } } /// A convenience function for building `N` vector masks from a fat /// `Teddy` value. /// /// # Panics /// /// When `V` represents a vector bigger than what `MaskBytes` can contain. /// /// # Safety /// /// Callers must ensure that this is okay to call in the current target for /// the current CPU. #[inline(always)] unsafe fn from_teddy( teddy: &Teddy<16>, ) -> [Mask; BYTES] { // MSRV(1.63): Use core::array::from_fn to just build the array here // instead of creating a vector and turning it into an array. let mut mask_builders = vec![FatMaskBuilder::default(); BYTES]; for (bucket_index, bucket) in teddy.buckets.iter().enumerate() { for pid in bucket.iter().copied() { let pat = teddy.patterns.get(pid); for (i, builder) in mask_builders.iter_mut().enumerate() { builder.add(bucket_index, pat.bytes()[i]); } } } let array = <[FatMaskBuilder; BYTES]>::try_from(mask_builders).unwrap(); array.map(|builder| builder.build()) } } impl Debug for FatMaskBuilder { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let (mut parts_lo, mut parts_hi) = (vec![], vec![]); for i in 0..32 { parts_lo.push(format!("{:02}: {:08b}", i, self.lo[i])); parts_hi.push(format!("{:02}: {:08b}", i, self.hi[i])); } f.debug_struct("FatMaskBuilder") .field("lo", &parts_lo) .field("hi", &parts_hi) .finish() } }