Skip to main content

writerslogic_warm/core/
intersection.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Sparse intersection primitives.
5//!
6//! Galloping intersection for skewed sizes, sorted merge for balanced.
7
8/// Count common elements in two sorted u32 slices.
9///
10/// Adaptively picks the best algorithm:
11/// - When one slice is much smaller (|a| * 8 < |b|), uses galloping search
12///   which is O(|small| * log(|large|)) and beats O(|a| + |b|) merge.
13/// - Otherwise uses the standard sorted merge at O(|a| + |b|).
14#[inline]
15pub fn sparse_intersection_count(a: &[u32], b: &[u32]) -> usize {
16    if a.is_empty() || b.is_empty() {
17        return 0;
18    }
19
20    // Ensure a is the smaller slice for the skew check.
21    let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
22    // Skewed sizes gallop before any block kernel: a block kernel walks the
23    // large side linearly.
24    if small.len() * 8 < large.len() {
25        return galloping_intersection_count(small, large);
26    }
27
28    #[cfg(target_arch = "x86_64")]
29    if let Some(count) = crate::core::simd_math::simd_intersection_count(small, large) {
30        return count;
31    }
32
33    merge_intersection_count(small, large)
34}
35
36/// Sorted merge intersection count: O(|a| + |b|).
37#[inline]
38fn merge_intersection_count(a: &[u32], b: &[u32]) -> usize {
39    let mut count = 0;
40    let mut i = 0;
41    let mut j = 0;
42    while i < a.len() && j < b.len() {
43        match a[i].cmp(&b[j]) {
44            std::cmp::Ordering::Less => i += 1,
45            std::cmp::Ordering::Greater => j += 1,
46            std::cmp::Ordering::Equal => {
47                count += 1;
48                i += 1;
49                j += 1;
50            }
51        }
52    }
53    count
54}
55
56/// Galloping (exponential search) intersection: O(|small| * log(|large|)).
57///
58/// For each element in `small`, does an exponential search in `large` to find
59/// the matching position. Maintains a cursor into `large` so total work on
60/// the large array is bounded by O(|small| * log(|large|/|small|)).
61#[inline]
62fn galloping_intersection_count(small: &[u32], large: &[u32]) -> usize {
63    let mut count = 0;
64    let mut lo = 0; // cursor into large; advances monotonically
65    for &val in small {
66        // Skip elements in large that are less than val using exponential search.
67        // First, find a bound by doubling the step.
68        let mut step = 1;
69        while lo + step < large.len() && large[lo + step] < val {
70            step *= 2;
71        }
72        // Binary search within [lo, min(lo+step, len))
73        let hi = (lo + step).min(large.len());
74        // Find first position >= val
75        lo = binary_search_left(&large[lo..hi], val) + lo;
76        if lo < large.len() && large[lo] == val {
77            count += 1;
78            lo += 1; // move past this match
79        }
80    }
81    count
82}
83
84/// Binary search for the leftmost position where `slice[pos] >= target`.
85#[inline]
86fn binary_search_left(slice: &[u32], target: u32) -> usize {
87    let mut lo = 0;
88    let mut hi = slice.len();
89    while lo < hi {
90        let mid = lo + (hi - lo) / 2;
91        if slice[mid] < target {
92            lo = mid + 1;
93        } else {
94            hi = mid;
95        }
96    }
97    lo
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn naive(a: &[u32], b: &[u32]) -> usize {
105        let set: std::collections::HashSet<u32> = b.iter().copied().collect();
106        a.iter().filter(|x| set.contains(x)).count()
107    }
108
109    struct Lcg(u64);
110    impl Lcg {
111        fn next(&mut self) -> u64 {
112            self.0 = self
113                .0
114                .wrapping_mul(6364136223846793005)
115                .wrapping_add(1442695040888963407);
116            self.0 >> 33
117        }
118    }
119
120    /// Sorted, deduplicated sample of `n` values below `universe` (`universe >= n`).
121    fn sample(rng: &mut Lcg, n: usize, universe: u64) -> Vec<u32> {
122        let mut set = std::collections::BTreeSet::new();
123        while set.len() < n {
124            set.insert((rng.next() % universe) as u32);
125        }
126        set.into_iter().collect()
127    }
128
129    fn check_all(a: &[u32], b: &[u32]) {
130        let want = naive(a, b);
131        assert_eq!(sparse_intersection_count(a, b), want, "dispatch a,b");
132        assert_eq!(sparse_intersection_count(b, a), want, "dispatch b,a");
133        assert_eq!(merge_intersection_count(a, b), want, "merge");
134        let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
135        assert_eq!(galloping_intersection_count(small, large), want, "gallop");
136        #[cfg(target_arch = "x86_64")]
137        if let Some(c) = crate::core::simd_math::simd_intersection_count(a, b) {
138            assert_eq!(c, want, "avx2");
139        }
140    }
141
142    #[test]
143    fn all_kernels_match_naive_count() {
144        for seed in 0..300u64 {
145            let mut rng = Lcg(seed);
146            let na = (rng.next() % 2001) as usize;
147            let nb = (rng.next() % 2001) as usize;
148            // A universe near max(na, nb) forces heavy overlap; a wide one forces little.
149            let universe = (na.max(nb) as u64 + 1) * (1 + rng.next() % 8);
150            check_all(
151                &sample(&mut rng, na, universe),
152                &sample(&mut rng, nb, universe),
153            );
154        }
155    }
156
157    #[test]
158    fn kernels_match_on_edge_shapes_and_tails() {
159        let mut rng = Lcg(7);
160        for na in 0..=17usize {
161            for nb in (0..=17usize).chain([64, 65, 600]) {
162                for universe in [40u64, 2000, 1 << 20] {
163                    let universe = universe.max(na.max(nb) as u64);
164                    check_all(
165                        &sample(&mut rng, na, universe),
166                        &sample(&mut rng, nb, universe),
167                    );
168                }
169            }
170        }
171        let ident = sample(&mut rng, 777, 5000);
172        check_all(&ident, &ident);
173        let evens: Vec<u32> = (0..1000).map(|x| x * 2).collect();
174        let odds: Vec<u32> = (0..1000).map(|x| x * 2 + 1).collect();
175        check_all(&evens, &odds);
176        check_all(&[], &odds);
177        check_all(&[], &[]);
178    }
179}