Skip to main content

molframe_core/column/encoded/
model.rs

1//! Per-column physical encodings.
2//!
3//! Each column is stored in whichever form suits the data it holds. Identifier
4//! and metadata columns repeat heavily — a chunk's model number is one value
5//! repeated four thousand times, and its alternate-location column is usually
6//! blank throughout — so storing them literally wastes memory that the
7//! geometric kernels would otherwise have as cache.
8//!
9//! The rule that governs the choice is not "compress everything":
10//!
11//! > **Columns a kernel reads on every iteration stay plain.** Encoding pays for
12//! > identifiers, flags and metadata. It does not pay for positions, where a
13//! > branch in the read path costs more than the bandwidth it saves and defeats
14//! > vectorisation outright.
15
16use super::super::bits::{bit_width, pack_mapped, unpack_one};
17use super::iter::ColumnIter;
18use crate::symbol::SymbolId;
19use std::mem::size_of;
20
21/// A value that can live in an encoded column.
22///
23/// Encodings work on the bit pattern, so anything with a lossless 64-bit
24/// representation qualifies. Bit-packing a float is legal and never chosen: the
25/// encoder's rules only reach for it where the values are small integers.
26pub trait ColumnValue: Copy + PartialEq {
27    /// Whether the bit pattern is an integer, so that narrowing it or taking
28    /// differences between successive values means something.
29    ///
30    /// False for floating point. Bit-packing a float would sometimes succeed —
31    /// small floats have leading zero bits — and would produce a column whose
32    /// size depended on the exponents in it, which is not an encoding anyone
33    /// chose.
34    const IS_INTEGER: bool;
35
36    /// The value as bits.
37    fn to_bits(self) -> u64;
38    /// The value from bits.
39    fn from_bits(bits: u64) -> Option<Self>;
40}
41
42macro_rules! unsigned_column_value {
43    ($($type:ty),*) => {
44        $(impl ColumnValue for $type {
45            const IS_INTEGER: bool = true;
46
47            fn to_bits(self) -> u64 {
48                u64::from(self)
49            }
50
51            fn from_bits(bits: u64) -> Option<Self> {
52                Self::try_from(bits).ok()
53            }
54        })*
55    };
56}
57
58macro_rules! signed_column_value {
59    ($($type:ty),*) => {
60        $(impl ColumnValue for $type {
61            const IS_INTEGER: bool = true;
62
63            fn to_bits(self) -> u64 {
64                i64::from(self).cast_unsigned()
65            }
66
67            fn from_bits(bits: u64) -> Option<Self> {
68                Self::try_from(bits.cast_signed()).ok()
69            }
70        })*
71    };
72}
73
74unsigned_column_value!(u8, u16, u32);
75signed_column_value!(i8, i16, i32);
76
77impl ColumnValue for u64 {
78    const IS_INTEGER: bool = true;
79
80    fn to_bits(self) -> u64 {
81        self
82    }
83
84    fn from_bits(bits: u64) -> Option<Self> {
85        Some(bits)
86    }
87}
88
89impl ColumnValue for i64 {
90    const IS_INTEGER: bool = true;
91
92    fn to_bits(self) -> u64 {
93        self.cast_unsigned()
94    }
95
96    fn from_bits(bits: u64) -> Option<Self> {
97        Some(bits.cast_signed())
98    }
99}
100
101impl ColumnValue for f32 {
102    const IS_INTEGER: bool = false;
103
104    fn to_bits(self) -> u64 {
105        u64::from(self.to_bits())
106    }
107
108    fn from_bits(bits: u64) -> Option<Self> {
109        u32::try_from(bits).ok().map(Self::from_bits)
110    }
111}
112
113impl ColumnValue for SymbolId {
114    const IS_INTEGER: bool = true;
115
116    fn to_bits(self) -> u64 {
117        u64::from(self.get())
118    }
119
120    fn from_bits(bits: u64) -> Option<Self> {
121        u32::try_from(bits).ok().map(Self::from_raw)
122    }
123}
124
125/// One column of a chunk, in whichever physical form fits its data.
126///
127/// # Examples
128///
129/// ```
130/// use molframe_core::column::EncodedColumn;
131///
132/// let repeated = EncodedColumn::encode(&[7u32; 4096]);
133/// assert!(repeated.is_constant());
134/// assert_eq!(repeated.get(4095), Some(7));
135///
136/// let varied = EncodedColumn::encode(&[1u32, 9, 3, 9]);
137/// assert_eq!(varied.get(2), Some(3));
138/// assert_eq!(varied.len(), 4);
139/// ```
140#[derive(Clone, PartialEq, Debug)]
141pub enum EncodedColumn<T> {
142    /// Values as they are. The only form a kernel reads directly.
143    Plain(Vec<T>),
144    /// One value for the whole column.
145    Constant {
146        /// The repeated value.
147        value: T,
148        /// How many times it repeats.
149        len: u32,
150    },
151    /// Values with the position each run ends at.
152    RunLength {
153        /// One value per run.
154        values: Vec<T>,
155        /// Position one past the last member of each run.
156        run_ends: Vec<u32>,
157    },
158    /// Narrow integers at a fixed bit width.
159    BitPacked {
160        /// The packed bits.
161        data: Vec<u8>,
162        /// Bits per value.
163        width: u8,
164        /// How many values are packed.
165        len: u32,
166    },
167    /// A first value and the difference to each successive one.
168    Delta {
169        /// The value at position zero.
170        first: T,
171        /// Differences between successive values.
172        deltas: Vec<i32>,
173        /// The absolute value at every `DELTA_CHECKPOINT_STRIDE`-th position.
174        ///
175        /// Without these, reading position `p` sums `p` deltas, so walking a
176        /// column by index costs `O(n^2)`. Entry `k` holds the value at
177        /// position `(k + 1) * DELTA_CHECKPOINT_STRIDE`, which bounds any read
178        /// to one stride of additions. At four bytes per delta and eight per
179        /// checkpoint the table adds about three percent to the encoding.
180        checkpoints: Vec<i64>,
181    },
182}
183
184impl<T: ColumnValue> EncodedColumn<T> {
185    /// The number of values held.
186    ///
187    #[must_use]
188    pub fn len(&self) -> usize {
189        match self {
190            Self::Plain(values) => values.len(),
191            Self::Constant { len, .. } | Self::BitPacked { len, .. } => *len as usize,
192            Self::RunLength { run_ends, .. } => match run_ends.last() {
193                Some(end) => *end as usize,
194                None => 0,
195            },
196            Self::Delta { deltas, .. } => deltas.len() + 1,
197        }
198    }
199
200    /// Returns true when the column holds no values.
201    #[must_use]
202    pub fn is_empty(&self) -> bool {
203        self.len() == 0
204    }
205
206    /// Returns true when the column is one repeated value.
207    #[must_use]
208    pub const fn is_constant(&self) -> bool {
209        matches!(self, Self::Constant { .. })
210    }
211
212    /// The values as a slice, when they are stored literally.
213    ///
214    /// A kernel that needs a slice asks for one and takes the other path when
215    /// it is refused; nothing forces a column into plain form behind the
216    /// caller's back.
217    #[must_use]
218    pub fn as_slice(&self) -> Option<&[T]> {
219        match self {
220            Self::Plain(values) => Some(values),
221            _ => None,
222        }
223    }
224
225    /// The value at `position`, or `None` if it is past the end.
226    #[must_use]
227    pub fn get(&self, position: u32) -> Option<T> {
228        if position as usize >= self.len() {
229            return None;
230        }
231
232        match self {
233            Self::Plain(values) => copied_at(values, position),
234            Self::Constant { value, .. } => Some(*value),
235            Self::RunLength { values, run_ends } => {
236                let run = run_ends.partition_point(|end| *end <= position);
237
238                values.get(run).copied()
239            }
240            Self::BitPacked { data, width, .. } => {
241                unpack_one(data, *width, position).and_then(T::from_bits)
242            }
243            Self::Delta {
244                first,
245                deltas,
246                checkpoints,
247            } => delta_at(*first, deltas, checkpoints, position),
248        }
249    }
250
251    /// Every value in order.
252    ///
253    /// Reading sequentially through a delta or run-length column this way is
254    /// linear overall, where repeated random access would not be.
255    pub fn iter(&self) -> ColumnIter<'_, T> {
256        ColumnIter::new(self)
257    }
258
259    /// Chooses an encoding for `values` by what they contain.
260    ///
261    /// The rules, in order: one repeated value becomes constant; few runs
262    /// become run-length; small non-negative integers become bit-packed when
263    /// that is genuinely narrower; a near-monotonic integer sequence becomes
264    /// deltas; anything else stays plain.
265    ///
266    #[must_use]
267    pub fn encode(values: &[T]) -> Self {
268        let Some(first) = values.first() else {
269            return Self::Plain(Vec::new());
270        };
271
272        if values.iter().skip(1).all(|value| value == first)
273            && let Ok(len) = u32::try_from(values.len())
274        {
275            return Self::Constant { value: *first, len };
276        }
277
278        if let Some(column) = Self::try_run_length(values) {
279            return column;
280        }
281
282        if let Some(column) = Self::try_bit_packed(values) {
283            return column;
284        }
285
286        if let Some(column) = Self::try_delta(values) {
287            return column;
288        }
289
290        Self::Plain(copy_values(values))
291    }
292
293    /// Stores values literally, whatever they are.
294    ///
295    /// This is what a hot column asks for: it declines encoding rather than
296    /// letting the rules decide.
297    #[must_use]
298    pub fn plain(values: &[T]) -> Self {
299        Self::Plain(copy_values(values))
300    }
301
302    fn try_run_length(values: &[T]) -> Option<Self> {
303        let runs = run_count(values);
304
305        // Each run costs a value and an end position, so it must replace more
306        // than two values' worth of storage to be worth the indirection.
307        if runs > values.len() / 4 {
308            return None;
309        }
310
311        let mut heads = Vec::with_capacity(runs);
312        let mut run_ends = Vec::with_capacity(runs);
313
314        for (position, value) in values.iter().enumerate() {
315            let end = u32::try_from(position.checked_add(1)?).ok()?;
316
317            if heads.last() == Some(value) {
318                if let Some(last_end) = run_ends.last_mut() {
319                    *last_end = end;
320                }
321
322                continue;
323            }
324
325            heads.push(*value);
326            run_ends.push(end);
327        }
328
329        Some(Self::RunLength {
330            values: heads,
331            run_ends,
332        })
333    }
334
335    fn try_bit_packed(values: &[T]) -> Option<Self> {
336        if !T::IS_INTEGER {
337            return None;
338        }
339
340        let max = values.iter().map(|value| value.to_bits()).max()?;
341
342        let width = bit_width(max);
343
344        if usize::from(width) >= size_of::<T>() * 8 {
345            return None;
346        }
347
348        Some(Self::BitPacked {
349            data: pack_mapped(values, width, |value| value.to_bits())?,
350            width,
351            len: u32::try_from(values.len()).ok()?,
352        })
353    }
354
355    fn try_delta(values: &[T]) -> Option<Self> {
356        if !T::IS_INTEGER {
357            return None;
358        }
359
360        let first = *values.first()?;
361        let delta_count = if values.is_empty() {
362            0
363        } else {
364            values.len() - 1
365        };
366        let mut deltas = Vec::with_capacity(delta_count);
367
368        for pair in values.windows(2) {
369            let [earlier, later] = pair else {
370                return None;
371            };
372
373            let step = later
374                .to_bits()
375                .cast_signed()
376                .checked_sub(earlier.to_bits().cast_signed())?;
377
378            deltas.push(i32::try_from(step).ok()?);
379        }
380
381        let mut checkpoints = Vec::with_capacity(deltas.len() / DELTA_CHECKPOINT_STRIDE);
382        let mut running = first.to_bits().cast_signed();
383        for (index, delta) in deltas.iter().enumerate() {
384            running = running.checked_add(i64::from(*delta))?;
385            if (index + 1).is_multiple_of(DELTA_CHECKPOINT_STRIDE) {
386                checkpoints.push(running);
387            }
388        }
389
390        Some(Self::Delta {
391            first,
392            deltas,
393            checkpoints,
394        })
395    }
396}
397
398fn run_count<T: PartialEq>(values: &[T]) -> usize {
399    if values.is_empty() {
400        return 0;
401    }
402
403    1 + values
404        .windows(2)
405        .filter(|pair| pair.first() != pair.get(1))
406        .count()
407}
408
409fn copy_values<T: Copy>(values: &[T]) -> Vec<T> {
410    values.to_vec()
411}
412
413fn copied_at<T: Copy>(values: &[T], position: u32) -> Option<T> {
414    usize::try_from(position)
415        .ok()
416        .and_then(|index| values.get(index))
417        .copied()
418}
419
420/// Positions between stored absolute values in a delta-encoded column.
421///
422/// Small enough that a read sums at most this many deltas, large enough that
423/// the checkpoint table stays a small fraction of the deltas themselves.
424const DELTA_CHECKPOINT_STRIDE: usize = 64;
425
426fn delta_at<T: ColumnValue>(
427    first: T,
428    deltas: &[i32],
429    checkpoints: &[i64],
430    position: u32,
431) -> Option<T> {
432    let end = usize::try_from(position).ok()?;
433    if end > deltas.len() {
434        return None;
435    }
436
437    // Resume from the last checkpoint at or before `position` rather than from
438    // the head of the column, bounding the walk to one stride.
439    let block = end / DELTA_CHECKPOINT_STRIDE;
440    let (start, base) = match block.checked_sub(1).and_then(|slot| checkpoints.get(slot)) {
441        Some(checkpoint) => (block * DELTA_CHECKPOINT_STRIDE, *checkpoint),
442        None => (0, first.to_bits().cast_signed()),
443    };
444
445    let value = deltas
446        .get(start..end)?
447        .iter()
448        .fold(base, |value, delta| value + i64::from(*delta));
449
450    T::from_bits(value.cast_unsigned())
451}
452
453#[cfg(test)]
454#[path = "model_tests.rs"]
455mod tests;