Skip to main content

rudb_kernels/
select.rs

1//! Turning a vector of flags into the rows it keeps.
2//!
3//! This is the other half of a filter. The comparison kernel produces a vector of booleans fast, and
4//! then something has to turn that vector into the positions that survived, which is what an
5//! operator hands downstream. Reading the flags back out one [`rudb_common::Value`] at a time
6//! undoes the comparison kernel's work and then some: on a two million row table a comparison that
7//! costs under a nanosecond a row was followed by a read that cost twenty seven.
8//!
9//! # Why the loop has no branch in it
10//!
11//! The obvious loop is `if kept { push(index) }`, and the branch in it is unpredictable by
12//! construction. A filter that keeps every row or no rows predicts perfectly and is also a filter
13//! nobody needed; the filters that matter keep some rows, and which rows is exactly the thing the
14//! data decides rather than the code. A mispredict is somewhere between fifteen and twenty cycles,
15//! so at thirty percent selectivity the branch alone can cost more than everything else in the loop.
16//!
17//! So every row writes its own index at the current length and only a row that is kept moves the
18//! length on. The write is unconditional and lands in the same cache line most of the time, the
19//! addition is of a zero or a one, and there is no branch for a predictor to get wrong. That is why
20//! [`rudb_vector::Selection::from_indices`] exists: the buffer is filled and counted here and handed
21//! over whole, rather than being pushed into one position at a time with a capacity check per row.
22//!
23//! Three valued logic is what makes this a kernel rather than a line. A filter keeps a row when the
24//! predicate is true, and null is not true, which is what makes `WHERE x <> 5` leave out the rows
25//! where `x` is null. So a row is kept when its flag is set and its validity bit is set, and the
26//! second half of that is the reason the null path reads a word of the mask at a time rather than
27//! asking the vector per row.
28
29use rudb_common::LogicalType;
30use rudb_vector::{Data, Form, Selection, Validity, Vector};
31
32use crate::fallback::{self, Kernel};
33use crate::logic::is_true;
34use crate::shape::{identity, nulls_of};
35
36/// The first `rows` positions of `flags` where the flag is true and not null.
37///
38/// A vector that is not boolean, or is in a form with no loop here, falls through to reading it a
39/// value at a time and records itself in [`crate::fallback`]. The answer is the same either way.
40#[must_use]
41pub fn selection(flags: &Vector, rows: usize) -> Selection {
42    let rows = rows.min(flags.len());
43    if let Some(kept) = swept(flags, rows) {
44        return kept;
45    }
46    // One vector in, so its form goes in both halves of the report rather than leaving a column of
47    // zeros next to every row of it.
48    fallback::record(Kernel::Select, flags.form(), flags.form());
49    Selection::from_predicate(rows, |index| is_true(&flags.value_at(index)))
50}
51
52fn swept(flags: &Vector, rows: usize) -> Option<Selection> {
53    if *flags.logical_type() != LogicalType::Boolean {
54        return None;
55    }
56    // The indices are written as `u32`, which is what a selection holds. A vector is 1024 rows and
57    // a row group is 122,880, so this is a bound the callers are nowhere near rather than a limit.
58    if rows > u32::MAX as usize {
59        return None;
60    }
61    match flags.form() {
62        // One value decides the whole vector, and the answer is every row or no rows.
63        Form::Constant => Some(if is_true(flags.constant_value()?) {
64            Selection::identity(rows)
65        } else {
66            Selection::empty()
67        }),
68        Form::Flat => {
69            let Data::Bool(values) = flags.data()? else {
70                return None;
71            };
72            if values.len() < rows {
73                return None;
74            }
75            Some(picked(values, identity, rows, &nulls_of(flags)))
76        }
77        Form::Dictionary => {
78            let (codes, inner) = flags.dictionary_parts()?;
79            if codes.len() < rows {
80                return None;
81            }
82            let Data::Bool(values) = inner.data()? else {
83                return None;
84            };
85            // Every code is inside the dictionary because `Vector::dictionary` checks that on the
86            // way in, so the gather below indexes without a bound of its own.
87            Some(picked(values, |index| codes[index] as usize, rows, &nulls_of(flags)))
88        }
89        _ => None,
90    }
91}
92
93#[expect(
94    clippy::cast_possible_truncation,
95    reason = "the caller checked that the row count fits in a u32 before getting here"
96)]
97fn picked<M: Fn(usize) -> usize>(
98    values: &[bool],
99    at: M,
100    rows: usize,
101    nulls: &Validity,
102) -> Selection {
103    let mut out = vec![0_u32; rows];
104    let mut kept = 0;
105    match nulls {
106        Validity::AllValid => {
107            for index in 0..rows {
108                out[kept] = index as u32;
109                kept += usize::from(values[at(index)]);
110            }
111        }
112        Validity::AllInvalid => {}
113        Validity::Mask(mask) => {
114            for start in (0..rows).step_by(64) {
115                let word = mask.word(start / 64);
116                for index in start..(start + 64).min(rows) {
117                    out[kept] = index as u32;
118                    let live = word >> (index - start) & 1 == 1;
119                    // A single `&` rather than `&&`, because the short circuit would put back the
120                    // branch this whole loop is shaped to avoid.
121                    kept += usize::from(live & values[at(index)]);
122                }
123            }
124        }
125    }
126    out.truncate(kept);
127    Selection::from_indices(out)
128}
129
130#[cfg(test)]
131mod tests {
132    use rudb_common::Value;
133
134    use super::*;
135
136    fn flags(values: &[Value]) -> Vector {
137        Vector::from_values(LogicalType::Boolean, values).expect("a vector of booleans")
138    }
139
140    const YES: Value = Value::Boolean(true);
141    const NO: Value = Value::Boolean(false);
142
143    /// The row at a time path, which is what the loop above has to agree with.
144    fn oracle(vector: &Vector, rows: usize) -> Selection {
145        Selection::from_predicate(rows, |index| is_true(&vector.value_at(index)))
146    }
147
148    struct Rng(u64);
149
150    impl Rng {
151        fn next(&mut self) -> u64 {
152            self.0 ^= self.0 << 13;
153            self.0 ^= self.0 >> 7;
154            self.0 ^= self.0 << 17;
155            self.0
156        }
157    }
158
159    #[test]
160    fn a_null_flag_is_not_a_true_flag() {
161        let vector = flags(&[YES, Value::Null, NO, YES]);
162        let kept = selection(&vector, 4);
163        assert_eq!(kept.indices(), &[0, 3]);
164        assert_eq!(kept, oracle(&vector, 4));
165    }
166
167    /// Every selectivity from nothing to everything, at four null densities, in both forms that
168    /// have a loop, against reading the flags a value at a time.
169    #[test]
170    fn the_rows_kept_are_the_rows_the_row_at_a_time_path_keeps() {
171        let _turn = fallback::TURN.lock().expect("no test panics while holding this");
172        let mut rng = Rng(0x5eed_ca11_ab1e_0005);
173        for nulls in [0_usize, 8, 3, 1] {
174            for share in [0_u64, 1, 16, 50, 84, 99, 100] {
175                let values: Vec<Value> = (0..251)
176                    .map(|index| {
177                        if nulls > 0 && index % nulls == 0 {
178                            Value::Null
179                        } else {
180                            Value::Boolean(rng.next() % 100 < share)
181                        }
182                    })
183                    .collect();
184                let vector = flags(&values);
185                let note = format!("{share} percent true, one null in {nulls}");
186                assert_eq!(selection(&vector, 251), oracle(&vector, 251), "{note}, flat");
187                let codes: Vec<u32> = (0..251).map(|index| (index % 37) as u32).collect();
188                let coded = Vector::dictionary(codes, vector).expect("codes are in range");
189                assert_eq!(selection(&coded, 251), oracle(&coded, 251), "{note}, dictionary");
190            }
191        }
192    }
193
194    #[test]
195    fn a_constant_is_answered_without_a_loop_and_a_non_boolean_is_not_answered_at_all() {
196        let _turn = fallback::TURN.lock().expect("no test panics while holding this");
197        fallback::reset();
198        let all = Vector::constant(LogicalType::Boolean, YES, 500);
199        assert_eq!(selection(&all, 500), Selection::identity(500));
200        let none = Vector::constant(LogicalType::Boolean, Value::Null, 500);
201        assert!(selection(&none, 500).is_empty());
202        assert_eq!(fallback::count(Kernel::Select, Form::Constant, Form::Constant), 0);
203
204        // Nothing binds a filter to a non boolean, and if something did it would be wrong rather
205        // than fast, so the loop refuses it and the value at a time path decides.
206        let numbers = Vector::from_values(LogicalType::Integer, &[Value::Integer(1)])
207            .expect("a vector of integers");
208        assert!(selection(&numbers, 1).is_empty());
209        assert_eq!(fallback::count(Kernel::Select, Form::Flat, Form::Flat), 1);
210        fallback::reset();
211    }
212
213    /// Fewer rows than the vector holds, which is what a partly filled chunk is.
214    #[test]
215    fn only_the_rows_asked_for_are_looked_at() {
216        let vector = flags(&[YES, YES, YES, YES]);
217        assert_eq!(selection(&vector, 2).indices(), &[0, 1]);
218        // And more rows than there are is the vector's length, not a panic.
219        assert_eq!(selection(&vector, 9).indices(), &[0, 1, 2, 3]);
220    }
221}