diff --git a/crates/dr256-ternlog-lab/examples/selection_forms.rs b/crates/dr256-ternlog-lab/examples/selection_forms.rs new file mode 100644 index 000000000..8b66636f3 --- /dev/null +++ b/crates/dr256-ternlog-lab/examples/selection_forms.rs @@ -0,0 +1,201 @@ +//! Measures three forms of the same slab planes: the contiguous words the +//! fold reads, the borrowed per-row `Selection` view, and one owned +//! `WideFieldMask` per row (the measured alternative, built here only). +//! Every arm's answer is checked against a row-at-a-time oracle first. + +use dr256_ternlog_lab::*; +use lance_graph_contract::class_view::WideFieldMask; +use lance_graph_dt64::dr::Scope; +use lance_graph_mask_risc::LaneRef; +use std::hint::black_box; +use std::time::Instant; + +fn mix(mut x: u64) -> u64 { + x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); + x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + x ^ (x >> 31) +} +/// 3 of 4 rows are present. +fn present(i: usize) -> bool { + !mix(i as u64).is_multiple_of(4) +} +/// 7 of 8 rows are in the reader's scope. +fn in_scope(i: usize) -> bool { + !mix(i as u64 ^ 0xA5A5).is_multiple_of(8) +} + +// One heap allocation per row: position 255 forces the four-chunk form. +fn owned(sel: Selection<'_>) -> WideFieldMask { + let mut m = WideFieldMask::EMPTY.with(255); + let w = m.words_mut(); + w.fill(0); + w[..sel.words().len()].copy_from_slice(sel.words()); + m +} + +fn fmt(ns: f64) -> String { + if ns < 10_000.0 { + format!("{ns:.0}ns") + } else { + format!("{:.2}\u{b5}s", ns / 1_000.0) + } +} + +/// Median and minimum of 31 samples, each `k` calls under one clock read. +fn measure(n: usize, arm: &str, k: u32, want: u64, f: &mut dyn FnMut() -> u64) -> f64 { + assert_eq!(f(), want, "arm {arm}"); + for _ in 0..5 { + black_box(f()); + } + let mut samples = Vec::with_capacity(31); + for _ in 0..31 { + let t = Instant::now(); + for _ in 0..k { + black_box(f()); + } + samples.push(t.elapsed().as_nanos() as f64 / f64::from(k)); + } + samples.sort_by(|a, b| a.partial_cmp(b).unwrap()); + println!( + "n={n} arm={arm} median={} min={}", + fmt(samples[15]), + fmt(samples[0]) + ); + samples[15] +} + +fn sum3(a: &[u64], b: &[u64], c: &[u64]) -> u64 { + a.iter() + .zip(b) + .zip(c) + .map(|((a, b), c)| u64::from((a & b & c).count_ones())) + .sum() +} + +fn main() { + let world = World::new(); + for n in [65_536usize, 1_000_000] { + let k = if n == 65_536 { 200 } else { 20 }; + let id: Vec = (0..n as u32).collect(); + let age: Vec = (0..n as i32).map(|i| 20 + (i % 60)).collect(); + let table = Table::new( + PERSON, + 1, + n, + 1, + Scope { id: 7 }, + &[ + (person::ID, LaneRef::U32(&id)), + (person::AGE, LaneRef::I32(&age)), + ], + ); + let mask = select(&world.view, PERSON, &["person_id", "age"]).unwrap(); + let prepared = prepare_nodes(&world, &table, &mask, present, in_scope).unwrap(); + let want = (0..n).filter(|&i| present(i) && in_scope(i)).count() as u64; + let slabs = prepared.slabs(); + + // The owned form, built once here; building it again is the build arm. + let build = || -> Vec<[Vec; 3]> { + (0..slabs) + .map(|s| { + let one = |p: usize| prepared.plane(s, p).selections().map(owned).collect(); + [one(0), one(1), one(2)] + }) + .collect() + }; + let owned_form = build(); + let values: usize = owned_form + .iter() + .map(|planes| planes.iter().map(Vec::len).sum::()) + .sum(); + let selections: usize = (0..slabs) + .flat_map(|s| (0..3).map(move |p| (s, p))) + .map(|(s, p)| prepared.plane(s, p).masks()) + .sum(); + assert_eq!(values, selections); + println!( + "n={n} slabs={slabs} selections={selections} contiguous_bytes={} owned_allocations={values} owned_bytes={}", + prepared.plane_bytes(), + values * (std::mem::size_of::() + 32) + ); + + let mut fold = Fold::count_and3().unwrap(); + measure(n, "fold/contiguous", k, want, &mut || { + fold.count(&world, &table, &prepared).unwrap() + }); + + let contig = measure(n, "words/contiguous", k, want, &mut || { + (0..slabs) + .map(|s| { + sum3( + &prepared.plane(s, 0).words, + &prepared.plane(s, 1).words, + &prepared.plane(s, 2).words, + ) + }) + .sum() + }); + + let view = measure(n, "words/borrowed_view", k, want, &mut || { + let mut total = 0u64; + for s in 0..slabs { + let (a, b, c) = ( + prepared.plane(s, 0), + prepared.plane(s, 1), + prepared.plane(s, 2), + ); + for y in 0..a.masks() { + let (x, y1, z) = (a.mask(y).unwrap(), b.mask(y).unwrap(), c.mask(y).unwrap()); + total += sum3(x.words(), y1.words(), z.words()); + } + } + total + }); + + let wide = measure(n, "words/owned_wide", k, want, &mut || { + let mut total = 0u64; + for planes in &owned_form { + for ((x, y1), z) in planes[0].iter().zip(&planes[1]).zip(&planes[2]) { + total += sum3(x.words(), y1.words(), z.words()); + } + } + total + }); + + // Three reusable contiguous buffers per slab, allocated outside the timing. + let mut buffers: Vec<[Vec; 3]> = (0..slabs) + .map(|s| { + let len = prepared.plane(s, 0).words.len(); + [vec![0; len], vec![0; len], vec![0; len]] + }) + .collect(); + let gather = measure(n, "gather/owned_wide", k, want, &mut || { + for (planes, bufs) in owned_form.iter().zip(buffers.iter_mut()) { + for p in 0..3 { + let len = bufs[p].len(); + for (y, m) in planes[p].iter().enumerate() { + let lo = y * MASK_WORDS; + let take = MASK_WORDS.min(len - lo); + bufs[p][lo..lo + take].copy_from_slice(&m.words()[..take]); + } + } + } + buffers.iter().map(|b| sum3(&b[0], &b[1], &b[2])).sum() + }); + + measure(n, "build/owned_wide", 1, values as u64, &mut || { + build() + .iter() + .map(|planes| planes.iter().map(Vec::len).sum::() as u64) + .sum() + }); + + println!( + "n={n} ratios borrowed_view/contiguous={:.2} owned_wide/contiguous={:.2} gather/contiguous={:.2}", + view / contig, + wide / contig, + gather / contig + ); + } +} diff --git a/crates/dr256-ternlog-lab/src/lib.rs b/crates/dr256-ternlog-lab/src/lib.rs index 53bc5169c..74ac0150c 100644 --- a/crates/dr256-ternlog-lab/src/lib.rs +++ b/crates/dr256-ternlog-lab/src/lib.rs @@ -3,7 +3,7 @@ //! ```text //! DT64 ClassView (the menu) what a field means, in which order //! DR WideFieldMask (the selection) which fields: storage, compute, display -//! Plane (one per slab) which rows: presence, selection, scope +//! Plane (one per slab) which rows: 256 selections of 256, borrowed //! RankMap (global -> local) where a global id sits after compaction //! exec mask-RISC ternlog(a, b, c) -> popcount, per slab //! ``` @@ -50,6 +50,12 @@ pub const SLAB_WORDS: usize = SLAB_ROWS / 64; pub const MAX_SLABS: usize = 256; /// `a & b & c` in the VPTERNLOG index convention. pub const AND3: u8 = 0x80; +/// Positions of one selection: one row of a slab. +pub const MASK_BITS: usize = 256; +/// Words of one selection, the chunk count of a 256-position `WideFieldMask`. +pub const MASK_WORDS: usize = MASK_BITS / 64; +/// Selections a full slab plane holds. +pub const SLAB_MASKS: usize = SLAB_ROWS / MASK_BITS; // ------------------------------------------------------------------ DT64 @@ -357,6 +363,7 @@ impl LaneSource for SlabSource<'_, '_> { // ---------------------------------------------------------------- planes /// One mask plane over one slab, with the row order it is in. +/// Read as 256 selections of 256 positions through [`Plane::mask`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Plane { pub space: CoordSpace, @@ -378,6 +385,88 @@ impl Plane { pub fn count(&self) -> u64 { self.words.iter().map(|w| u64::from(w.count_ones())).sum() } + + /// Selections this plane holds: one per 256 positions, the last one short + /// when the slab is. + pub fn masks(&self) -> usize { + self.words.len().div_ceil(MASK_WORDS) + } + + /// Selection `row` of this plane, borrowed in place. `None` past the plane + /// or past row 255. + pub fn mask(&self, row: usize) -> Option> { + let at = u8::try_from(row).ok()?; + let lo = row * MASK_WORDS; + if lo >= self.words.len() { + return None; + } + let hi = (lo + MASK_WORDS).min(self.words.len()); + Some(Selection { + space: self.space, + row: at, + words: &self.words[lo..hi], + }) + } + + /// Every selection, in row order. + pub fn selections(&self) -> impl Iterator> + '_ { + (0..self.masks()).filter_map(|row| self.mask(row)) + } +} + +/// One reusable selection of a slab plane: 256 positions, borrowed from the +/// plane's own words, with the row order it is in. The words are the chunks +/// of a `WideFieldMask` over the same positions (chunk `k` = positions +/// `64k..64k+63`); nothing is copied to read them. +/// +/// A plane's last selection is short when the slab is: fewer than four +/// words, and every position past the slab's rows reads absent. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Selection<'p> { + space: CoordSpace, + row: u8, + words: &'p [u64], +} + +impl<'p> Selection<'p> { + /// The row order the selection is in: its plane's. + pub fn space(&self) -> CoordSpace { + self.space + } + + /// Which of the plane's selections this is. + pub fn row(&self) -> u8 { + self.row + } + + /// The chunks, borrowed from the plane. At most [`MASK_WORDS`]. + pub fn words(&self) -> &'p [u64] { + self.words + } + + /// Is position `n` selected? + pub fn has(&self, n: u8) -> bool { + self.words + .get(usize::from(n) / 64) + .is_some_and(|w| w >> (n % 64) & 1 == 1) + } + + /// Selected positions. + pub fn count(&self) -> u32 { + self.words.iter().map(|w| w.count_ones()).sum() + } + + pub fn is_empty(&self) -> bool { + self.words.iter().all(|&w| w == 0) + } + + /// Whether `mask` selects exactly these positions, whatever width either + /// side stores. Reads both in place. + pub fn same_bits(&self, mask: &WideFieldMask) -> bool { + let other = mask.words(); + (0..self.words.len().max(other.len())) + .all(|i| self.words.get(i).copied().unwrap_or(0) == other.get(i).copied().unwrap_or(0)) + } } /// Global to local as a mask: a global id's local row is its rank among the diff --git a/crates/dr256-ternlog-lab/tests/lab.rs b/crates/dr256-ternlog-lab/tests/lab.rs index ce4edf566..baec13ba0 100644 --- a/crates/dr256-ternlog-lab/tests/lab.rs +++ b/crates/dr256-ternlog-lab/tests/lab.rs @@ -5,7 +5,7 @@ use dr256_ternlog_lab::*; use lance_graph_contract::class_view::WideFieldMask; use lance_graph_dt64::dr::Scope; -use lance_graph_dt64::{LaneRefusal, Refusal}; +use lance_graph_dt64::{CoordSpace, LaneRefusal, Refusal}; use lance_graph_mask_risc::LaneRef; const READER: Scope = Scope { id: 7 }; @@ -578,3 +578,135 @@ fn an_order_index_refuses_a_table_it_was_not_built_over() { 1 ); } + +#[test] +fn a_plane_reads_as_256_selections_of_256_in_place() { + let world = World::new(); + let people = People::new(70_000); + let table = people.table(); + let prepared = prepare_nodes(&world, &table, &People::mask(&world), present, in_scope).unwrap(); + let preds: [fn(usize) -> bool; 2] = [present, in_scope]; + for s in 0..2usize { + for (p, pred) in preds.iter().enumerate() { + let plane = prepared.plane(s, p); + assert_eq!(plane.masks(), table.slab_rows(s).div_ceil(256)); + assert_eq!(plane.masks(), [256, 18][s]); + for y in 0..plane.masks() { + let sel = plane.mask(y).unwrap(); + assert_eq!(sel.row() as usize, y); + assert_eq!(sel.space(), table.space(s)); + assert!(std::ptr::eq( + sel.words().as_ptr(), + plane.words[y * MASK_WORDS..].as_ptr() + )); + for x in 0..=255u8 { + let i = y * 256 + x as usize; + let want = i < table.slab_rows(s) && pred(s * SLAB_ROWS + i); + assert_eq!(sel.has(x), want, "s={s} p={p} y={y} x={x}"); + } + } + let sum: u64 = plane.selections().map(|m| u64::from(m.count())).sum(); + assert_eq!(sum, plane.count()); + assert!(sum > 0 && sum < table.slab_rows(s) as u64); + } + } +} + +#[test] +fn a_selection_past_the_plane_is_absent_and_the_last_one_is_short() { + let space = CoordSpace { + population: 7, + coordinates: 1, + }; + let two = Plane::from_fn(space, 512, present); + assert_eq!(two.masks(), 2); + assert_eq!(two.mask(1).unwrap().words().len(), 4); + assert!(two.mask(2).is_none()); + assert_eq!(two.selections().count(), 2); + + let short = Plane::from_fn(space, 4_464, present); + assert_eq!(short.words.len(), 70); + assert_eq!(short.masks(), 18); + let last = short.mask(17).unwrap(); + assert_eq!(last.words().len(), 2); + let want = (4_352..4_464).filter(|&i| present(i)).count() as u32; + assert!(want > 0); + assert_eq!(last.count(), want); + assert!(!last.has(200)); + assert!(short.mask(18).is_none()); + + let full = Plane { + space, + words: vec![u64::MAX; 1_028], + }; + assert_eq!(full.mask(255).unwrap().count(), 256); + assert!(full.mask(256).is_none()); +} + +#[test] +fn a_selection_equals_the_wide_field_mask_of_the_same_positions() { + let space = CoordSpace { + population: 7, + coordinates: 1, + }; + let rule = |y: usize, x: usize| match y { + 0 => x < 64 && x.is_multiple_of(3), + 1 => x.is_multiple_of(5), + 2 => x >= 200, + _ => false, + }; + let plane = Plane::from_fn(space, 1_024, |i| rule(i / 256, i % 256)); + let positions = |y: usize| -> Vec { + (0..256usize) + .filter(|&x| rule(y, x)) + .map(|x| x as u8) + .collect() + }; + let masks: Vec = (0..4) + .map(|y| WideFieldMask::from_positions(&positions(y))) + .collect(); + for (y, mask) in masks.iter().enumerate() { + let sel = plane.mask(y).unwrap(); + assert!(sel.same_bits(mask), "row {y}"); + assert_eq!(sel.count(), mask.count(), "row {y}"); + } + let r0 = plane.mask(0).unwrap(); + assert_eq!(masks[0].words().len(), 1); + assert_eq!(r0.words().len(), 4); + assert_eq!(plane.mask(1).unwrap().words(), masks[1].words()); + let r3 = plane.mask(3).unwrap(); + assert!(r3.is_empty()); + assert!(r3.same_bits(&WideFieldMask::EMPTY)); + + assert!(!r0.same_bits(&masks[0].clone().with(100))); + let p1 = positions(1); + assert!(!plane + .mask(1) + .unwrap() + .same_bits(&WideFieldMask::from_positions(&p1[1..]))); + assert!(!plane.mask(2).unwrap().same_bits(&WideFieldMask::EMPTY)); + assert!(!r3.same_bits(&WideFieldMask::EMPTY.with(0))); +} + +#[test] +fn selections_carry_the_coordinate_space_of_their_plane() { + let world = World::new(); + let people = People::new(2 * SLAB_ROWS); + let table = people.table(); + let mask = People::mask(&world); + let prepared = prepare_nodes(&world, &table, &mask, present, in_scope).unwrap(); + let index = OrderIndex::by_i32(&table, person::AGE, 99).unwrap(); + let order = prepare_order(&world, &table, &mask, &index, present).unwrap(); + + let a = prepared.plane(0, 0).mask(3).unwrap(); + let b = prepared.plane(1, 0).mask(3).unwrap(); + assert_eq!(a.row(), b.row()); + assert_eq!(a.space(), table.space(0)); + assert_eq!(b.space(), table.space(1)); + assert_ne!(a.space(), b.space()); + assert_eq!(prepared.plane(0, 1).mask(3).unwrap().space(), a.space()); + + let o = order.plane(0).mask(3).unwrap(); + assert_ne!(o.space(), a.space()); + assert_eq!(o.space().population, 99 << 8); +}