11use crate :: postings:: compression:: COMPRESSION_BLOCK_SIZE ;
22
3- /// Search the first index containing an element greater or equal to
4- /// the target.
3+ /// Returns the index of the first element in `arr` that is greater than or
4+ /// equal to ` target` .
55///
6- /// The results should be equivalent to
7- /// ```compile_fail
8- /// block[..]
9- // .iter()
10- // .take_while(|&&val| val < target)
11- // .count()
6+ /// This is equivalent to:
7+ ///
8+ /// ```ignore
9+ /// arr.iter().take_while(|&&val| val < target).count()
1210/// ```
13- ///
14- /// the `start` argument is just used to hint that the response is
15- /// greater than beyond `start`. The implementation may or may not use
16- /// it for optimization.
1711///
18- /// # Assumption
12+ /// # Assumptions
13+ ///
14+ /// - `arr` is sorted in nondecreasing order. Values may be repeated; the last block is often padded
15+ /// with duplicates of its final value.
16+ /// - `target` is less than or equal to the last element in `arr`, so the result is always a valid
17+ /// index into the block.
18+ ///
19+ /// # `K`
1920///
20- /// - The block is sorted. Some elements may appear several times. This is the case at the
21- /// end of the last block for instance.
22- /// - The target is assumed smaller or equal to the last element of the block.
23- pub fn branchless_binary_search ( arr : & [ u32 ; COMPRESSION_BLOCK_SIZE ] , target : u32 ) -> usize {
24- let mut start = 0 ;
25- let mut len = arr. len ( ) ;
26- for _ in 0 ..7 {
27- len /= 2 ;
28- let pivot = unsafe { * arr. get_unchecked ( start + len - 1 ) } ;
29- if pivot < target {
30- start += len;
21+ /// `K` is the branching factor. Each reduction probes `K - 1` segment-end
22+ /// pivots, keeps the matching segment, and finally linearly scans the remaining
23+ /// range. `K` must be one of `2`, `4`, `8`, `16`, `32`, or `64`.
24+ ///
25+ /// The core idea vs a traditional binary search is that we can very cheaply scan blocks of
26+ /// numbers, since they are already in the CPU cache line.
27+ #[ inline( always) ]
28+ pub fn kary_search < const K : usize > ( arr : & [ u32 ; COMPRESSION_BLOCK_SIZE ] , target : u32 ) -> usize {
29+ const {
30+ assert ! (
31+ matches!( K , 2 | 4 | 8 | 16 | 32 | 64 ) ,
32+ "K must be one of 2, 4, 8, 16, 32, or 64"
33+ ) ;
34+ } ;
35+
36+ let mut base = 0usize ;
37+ let mut range = COMPRESSION_BLOCK_SIZE ;
38+
39+ loop {
40+ let step = range / K ;
41+ if step == 0 {
42+ break ;
3143 }
44+ debug_assert_eq ! ( range % K , 0 ) ;
45+ // Count how many segment-end pivots are < target (branchless, unrolled).
46+ let mut count = 0usize ;
47+ for i in 1 ..K {
48+ count += ( unsafe { * arr. get_unchecked ( base + i * step - 1 ) } < target) as usize ;
49+ }
50+ base += count * step;
51+ range = step;
52+ }
53+
54+ // Linear scan over the ≤K remaining elements.
55+ let mut count = 0usize ;
56+ for i in 0 ..range {
57+ count += ( unsafe { * arr. get_unchecked ( base + i) } < target) as usize ;
3258 }
33- start
59+ base + count
60+ }
61+
62+ /// entry point used by postings; implemented as an 8-ary branchless search.
63+ #[ inline]
64+ pub fn search_block ( arr : & [ u32 ; COMPRESSION_BLOCK_SIZE ] , target : u32 ) -> usize {
65+ kary_search :: < 8 > ( arr, target)
3466}
3567
3668#[ cfg( test) ]
@@ -39,7 +71,7 @@ mod tests {
3971
4072 use proptest:: prelude:: * ;
4173
42- use super :: branchless_binary_search ;
74+ use super :: { kary_search , search_block } ;
4375 use crate :: docset:: TERMINATED ;
4476 use crate :: postings:: compression:: COMPRESSION_BLOCK_SIZE ;
4577
@@ -57,7 +89,7 @@ mod tests {
5789 assert_eq ! ( block. len( ) , COMPRESSION_BLOCK_SIZE ) ;
5890 let mut output_buffer = [ TERMINATED ; COMPRESSION_BLOCK_SIZE ] ;
5991 output_buffer[ ..block. len ( ) ] . copy_from_slice ( block) ;
60- assert_eq ! ( branchless_binary_search ( & output_buffer, target) , cursor) ;
92+ assert_eq ! ( search_block ( & output_buffer, target) , cursor) ;
6193 }
6294
6395 fn util_test_search_in_block_all ( block : & [ u32 ] ) {
@@ -80,6 +112,45 @@ mod tests {
80112 util_test_search_in_block_all ( & v[ ..] ) ;
81113 }
82114
115+ #[ test]
116+ fn test_search_in_branchless_binary_search_corner_cases ( ) {
117+ let all_same = vec ! [ 7u32 ; COMPRESSION_BLOCK_SIZE ] ;
118+ util_test_search_in_block_all ( & all_same) ;
119+
120+ let repeated_across_pivots: Vec < u32 > = ( 0 ..COMPRESSION_BLOCK_SIZE )
121+ . map ( |i| ( i / 17 ) as u32 )
122+ . collect ( ) ;
123+ util_test_search_in_block_all ( & repeated_across_pivots) ;
124+
125+ let mut padded_last_block = vec ! [ 0u32 ; COMPRESSION_BLOCK_SIZE ] ;
126+ for ( i, value) in padded_last_block. iter_mut ( ) . enumerate ( ) {
127+ * value = if i < COMPRESSION_BLOCK_SIZE / 2 {
128+ i as u32
129+ } else {
130+ TERMINATED
131+ } ;
132+ }
133+ util_test_search_in_block_all ( & padded_last_block) ;
134+ }
135+
136+ #[ test]
137+ fn test_kary_search_allowed_branching_factors ( ) {
138+ let mut block = [ TERMINATED ; COMPRESSION_BLOCK_SIZE ] ;
139+ for ( idx, value) in block. iter_mut ( ) . enumerate ( ) {
140+ * value = ( idx / 3 ) as u32 ;
141+ }
142+
143+ for target in [ 0 , 1 , 17 , block[ COMPRESSION_BLOCK_SIZE - 1 ] ] {
144+ let expected = search_in_block_trivial_but_slow ( & block, target) ;
145+ assert_eq ! ( kary_search:: <2 >( & block, target) , expected) ;
146+ assert_eq ! ( kary_search:: <4 >( & block, target) , expected) ;
147+ assert_eq ! ( kary_search:: <8 >( & block, target) , expected) ;
148+ assert_eq ! ( kary_search:: <16 >( & block, target) , expected) ;
149+ assert_eq ! ( kary_search:: <32 >( & block, target) , expected) ;
150+ assert_eq ! ( kary_search:: <64 >( & block, target) , expected) ;
151+ }
152+ }
153+
83154 fn monotonous_block ( ) -> impl Strategy < Value = Vec < u32 > > {
84155 prop:: collection:: vec ( 0u32 ..5u32 , COMPRESSION_BLOCK_SIZE ) . prop_map ( |mut deltas| {
85156 let mut el = 0 ;
0 commit comments