Skip to content

Commit 82bee54

Browse files
committed
block_search: drop unsafe indexing, remove K=64
For K∈{2,4,8,16,32} LLVM proves the index bounds and elides the checks, so get_unchecked buys nothing on the production K=8 path. K=64 was the only value that defeated bounds-check elision (one check in the tail scan) and it was instantiated in tests only — drop it. block_search: cite the k-ary search paper Document that kary_search is the 'k-ary search on a sorted array' variant from Schlegel, Gemulla & Lehner (DaMoN 2009), specialized to a lower-bound.
1 parent 6892995 commit 82bee54

1 file changed

Lines changed: 17 additions & 8 deletions

File tree

src/postings/block_search.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,26 @@ use crate::postings::compression::COMPRESSION_BLOCK_SIZE;
2020
///
2121
/// `K` is the branching factor. Each reduction probes `K - 1` segment-end
2222
/// 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`.
23+
/// range. `K` must be one of `2`, `4`, `8`, `16`, or `32`.
2424
///
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.
25+
/// This is the "k-ary search on a sorted array" variant of Schlegel, Gemulla & Lehner,
26+
/// "k-Ary Search on Modern Processors", DaMoN 2009 (<https://dl.acm.org/doi/10.1145/1565694.1565705>),
27+
/// specialized to a lower-bound (no equality early-exit) with a linear scan over the final
28+
/// `< K` elements. We do not use their linearized-tree (`k-ary-lt`) layout, which would require
29+
/// reordering the block.
30+
///
31+
/// The core idea vs a traditional binary search is that we can check multiple numbers in parallel,
32+
/// which better utilizes the CPU's instruction-level parallelism.
33+
///
34+
/// `kary_search::<8>` reduces in three steps: 128 -> 16 -> 2, then a 2-element scan. It could be
35+
/// done in only two steps (128 -> 16, then scanning all 16 contiguous elements). For that
36+
/// we need popcount for that to be fast though (TODO).
2737
#[inline(always)]
2838
pub fn kary_search<const K: usize>(arr: &[u32; COMPRESSION_BLOCK_SIZE], target: u32) -> usize {
2939
const {
3040
assert!(
31-
matches!(K, 2 | 4 | 8 | 16 | 32 | 64),
32-
"K must be one of 2, 4, 8, 16, 32, or 64"
41+
matches!(K, 2 | 4 | 8 | 16 | 32),
42+
"K must be one of 2, 4, 8, 16, or 32"
3343
);
3444
};
3545

@@ -45,7 +55,7 @@ pub fn kary_search<const K: usize>(arr: &[u32; COMPRESSION_BLOCK_SIZE], target:
4555
// Count how many segment-end pivots are < target (branchless, unrolled).
4656
let mut count = 0usize;
4757
for i in 1..K {
48-
count += (unsafe { *arr.get_unchecked(base + i * step - 1) } < target) as usize;
58+
count += (arr[base + i * step - 1] < target) as usize;
4959
}
5060
base += count * step;
5161
range = step;
@@ -54,7 +64,7 @@ pub fn kary_search<const K: usize>(arr: &[u32; COMPRESSION_BLOCK_SIZE], target:
5464
// Linear scan over the ≤K remaining elements.
5565
let mut count = 0usize;
5666
for i in 0..range {
57-
count += (unsafe { *arr.get_unchecked(base + i) } < target) as usize;
67+
count += (arr[base + i] < target) as usize;
5868
}
5969
base + count
6070
}
@@ -147,7 +157,6 @@ mod tests {
147157
assert_eq!(kary_search::<8>(&block, target), expected);
148158
assert_eq!(kary_search::<16>(&block, target), expected);
149159
assert_eq!(kary_search::<32>(&block, target), expected);
150-
assert_eq!(kary_search::<64>(&block, target), expected);
151160
}
152161
}
153162

0 commit comments

Comments
 (0)