use std::mem::size_of;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use rudb_common::{Error, Memory, Reservation, Result, Spent, Stage, stage};
use rudb_pipeline::Lease;
use crate::key::{mix, spread};
pub(crate) const PARTITIONS: usize = 64;
pub(crate) const ROWS_PER_PARTITION: usize = 16_384;
pub(crate) const ROWS_PER_EXTRA_THREAD: usize = 65_536;
pub(crate) fn finish_degree(input: usize, ceiling: usize) -> usize {
let quickly = input.div_ceil(ROWS_PER_PARTITION).min(8);
let slowly = input.div_ceil(ROWS_PER_EXTRA_THREAD);
quickly.max(slowly).clamp(1, ceiling)
}
const PIECES_PER_THREAD: usize = 4;
pub(crate) fn used(rows: usize, degree: usize) -> usize {
let cache = rows.div_ceil(ROWS_PER_PARTITION);
let balance = if degree > 1 { degree.saturating_mul(PIECES_PER_THREAD) } else { 1 };
cache.max(balance).max(1).next_power_of_two().min(PARTITIONS)
}
const EMPTY: u32 = u32::MAX;
const NOTHING: u64 = 0x9e37_79b9_7f4a_7c15;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Record {
pub(crate) user: i64,
pub(crate) group: i32,
pub(crate) pair_hash: u32,
}
#[inline]
pub(crate) fn group_seed(group: i32, valid: bool) -> u64 {
let word = if valid { i64::from(group) as u64 } else { NOTHING };
spread(mix(0, word))
}
#[inline]
pub(crate) fn folded(wide: u64) -> u32 {
(wide ^ (wide >> 32)) as u32
}
pub(crate) fn group_hash(group: i32, valid: bool) -> u32 {
folded(group_seed(group, valid))
}
#[derive(Debug, Default)]
pub(crate) struct Run {
pub(crate) rows: Vec<Record>,
pub(crate) validity: Vec<bool>,
}
const COMPACT_FROM: usize = 4_096;
impl Run {
#[inline]
pub(crate) fn push(&mut self, row: Record, valid: bool) {
if self.rows.len() == self.rows.capacity() && self.rows.len() >= COMPACT_FROM {
self.compact();
}
self.rows.push(row);
if self.validity.is_empty() {
if valid {
return;
}
self.validity = vec![true; self.rows.len() - 1];
}
self.validity.push(valid);
}
#[cold]
#[inline(never)]
fn compact(&mut self) {
let len = self.rows.len();
let capacity = len.saturating_mul(2).next_power_of_two();
let mask = capacity - 1;
let mut buckets = vec![EMPTY; capacity];
let all_valid = self.validity.is_empty();
let mut kept = 0;
for at in 0..len {
let row = self.rows[at];
let valid = all_valid || self.validity[at];
let mut slot = row.pair_hash as usize & mask;
loop {
let held = buckets[slot];
if held == EMPTY {
buckets[slot] = kept as u32;
self.rows[kept] = row;
if !all_valid {
self.validity[kept] = valid;
}
kept += 1;
break;
}
let other = self.rows[held as usize];
if other.user == row.user
&& other.group == row.group
&& (all_valid || self.validity[held as usize] == valid)
{
break;
}
slot = (slot + 1) & mask;
}
}
self.rows.truncate(kept);
self.validity.truncate(kept);
if kept * 4 > len * 3 {
self.rows.reserve(len);
}
}
fn valid_at(&self, row: usize) -> bool {
self.validity.is_empty() || self.validity[row]
}
pub(crate) fn footprint(&self) -> usize {
self.rows.capacity() * size_of::<Record>() + self.validity.capacity() * size_of::<bool>()
}
}
pub(crate) fn merged(at: usize, used: usize) -> std::ops::Range<usize> {
let per = PARTITIONS / used;
(at * per)..((at + 1) * per)
}
#[derive(Debug, Default)]
pub(crate) struct Held {
pub(crate) runs: Vec<Run>,
}
impl Held {
pub(crate) fn rows(&self) -> usize {
self.runs.iter().map(|run| run.rows.len()).sum()
}
}
#[inline]
pub(crate) fn scatter(partitions: &mut [Run], shift: u32, group: i32, valid: bool, user: i64) {
scatter_seeded(partitions, shift, group_seed(group, valid), group, valid, user);
}
#[inline]
pub(crate) fn scatter_seeded(
partitions: &mut [Run],
shift: u32,
seed: u64,
group: i32,
valid: bool,
user: i64,
) {
let pair_hash = folded(spread(mix(seed, user as u64)));
partitions[(pair_hash >> shift) as usize].push(Record { user, group, pair_hash }, valid);
}
pub(crate) fn shift() -> u32 {
u32::BITS - PARTITIONS.ilog2()
}
pub(crate) fn in_parallel<T: Send>(
threads: &Lease<'_>,
count: usize,
degree: usize,
what: &str,
run: impl Fn(usize) -> Result<T> + Sync,
) -> Result<Vec<T>> {
let next = AtomicUsize::new(0);
let slots: Vec<Mutex<Option<Result<T>>>> = (0..count).map(|_| Mutex::new(None)).collect();
let step = || {
loop {
let at = next.fetch_add(1, Ordering::Relaxed);
if at >= count {
return;
}
let done = run(at);
if let Ok(mut slot) = slots[at].lock() {
*slot = Some(done);
}
}
};
together(threads, degree.min(count), &step)?;
let mut out = Vec::with_capacity(count);
for (at, slot) in slots.iter().enumerate() {
out.push(
slot.lock()
.map_err(poisoned)?
.take()
.unwrap_or_else(|| Err(Error::internal(format!("nothing {what} {at}"))))?,
);
}
Ok(out)
}
pub(crate) fn together(threads: &Lease<'_>, degree: usize, work: &(dyn Fn() + Sync)) -> Result<()> {
let theirs = Mutex::new(Spent::none());
let task = || {
let before = stage::here();
work();
let mine = stage::here().since(before);
if let Ok(mut held) = theirs.lock() {
held.add(mine);
}
};
let (_, panicked) = threads.scatter_at_most(degree, &task, work);
stage::gained(theirs.into_inner().map_err(poisoned)?);
if panicked {
return Err(Error::internal("a thread finishing an aggregate panicked"));
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct Counted {
pub(crate) splits: Vec<Vec<Grouped>>,
pub(crate) held: Reservation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Grouped {
pub(crate) group: i32,
pub(crate) group_hash: u32,
pub(crate) valid: bool,
}
#[inline]
pub(crate) fn split_of(group_hash: u32, splits: usize) -> usize {
((u64::from(group_hash) * splits as u64) >> u32::BITS) as usize
}
pub(crate) fn distinct_pairs(
partition: &mut Held,
splits: usize,
memory: &Memory,
) -> Result<Counted> {
let reserving = stage::Timing::start(Stage::Reserve);
let held_rows = partition.rows();
let pair_capacity = held_rows.saturating_mul(2).max(64).next_power_of_two();
let mut working = memory.reservation();
working.grow(width(pair_capacity * size_of::<u32>()))?;
let mut pair_buckets = vec![EMPTY; pair_capacity];
let pair_mask = pair_capacity - 1;
let index_mask = u32::try_from(pair_mask)
.map_err(|_| Error::out_of_memory("a radix pair partition is too large"))?;
let tag_mask = !index_mask;
let all_valid = partition.runs.iter().all(|run| run.validity.is_empty());
let mut unique: Vec<Record> = Vec::with_capacity(held_rows);
let mut unique_validity: Vec<bool> =
if all_valid { Vec::new() } else { Vec::with_capacity(held_rows) };
working.grow(width(
unique.capacity() * size_of::<Record>() + unique_validity.capacity() * size_of::<bool>(),
))?;
let even = held_rows.div_ceil(splits);
let share = (even + even.isqrt() * 4).min(held_rows);
let mut parts: Vec<Vec<Grouped>> = (0..splits).map(|_| Vec::with_capacity(share)).collect();
reserving.stop(0);
let timing = stage::Timing::start(Stage::Fold);
for run in &partition.runs {
for (source, &row) in run.rows.iter().enumerate() {
let valid = all_valid || run.valid_at(source);
let tag = row.pair_hash & tag_mask;
let mut at = row.pair_hash as usize & pair_mask;
loop {
let slot = pair_buckets[at];
if slot == EMPTY {
pair_buckets[at] = tag | unique.len() as u32;
unique.push(row);
if !all_valid {
unique_validity.push(valid);
}
let group_hash = group_hash(row.group, valid);
let split = split_of(group_hash, splits);
parts[split].push(Grouped { group: row.group, group_hash, valid });
break;
}
if slot & tag_mask == tag {
let held_at = (slot & index_mask) as usize;
let held = unique[held_at];
let held_valid = all_valid || unique_validity[held_at];
if held.group == row.group && held.user == row.user && held_valid == valid {
break;
}
}
at = (at + 1) & pair_mask;
}
}
}
timing.stop(0);
let reserving = stage::Timing::start(Stage::Reserve);
partition.runs.clear();
drop(unique);
drop(unique_validity);
for split in &mut parts {
if split.capacity() > split.len().saturating_mul(2) {
split.shrink_to_fit();
}
}
let mut held = memory.reservation();
held.grow(width(
parts.iter().map(|split| split.capacity() * size_of::<Grouped>()).sum::<usize>(),
))?;
reserving.stop(0);
Ok(Counted { splits: parts, held })
}
fn width(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
fn poisoned<T>(_: T) -> Error {
Error::internal("a radix pair lock was poisoned")
}
#[cfg(test)]
mod tests {
use super::{
COMPACT_FROM, Held, PARTITIONS, ROWS_PER_PARTITION, Record, Run, distinct_pairs, folded,
group_hash, group_seed, merged, mix, scatter, scatter_seeded, shift, spread, used,
};
#[test]
fn the_partitions_the_finish_does_not_want_are_merged_into_the_ones_it_does() {
assert_eq!(used(0, 1), 1);
assert_eq!(used(1, 1), 1);
assert_eq!(used(ROWS_PER_PARTITION, 1), 1);
assert_eq!(used(ROWS_PER_PARTITION + 1, 1), 2);
assert_eq!(used(999_975, 32), PARTITIONS);
assert_eq!(used(usize::MAX, 32), PARTITIONS);
assert_eq!(used(ROWS_PER_PARTITION, 4), 16);
assert_eq!(used(0, 32), PARTITIONS);
for rows in [0, 1, 40_000, 999_975, usize::MAX] {
for degree in [1, 2, 4, 32] {
let taken = used(rows, degree);
let covered: Vec<usize> = (0..taken).flat_map(|at| merged(at, taken)).collect();
assert_eq!(covered, (0..PARTITIONS).collect::<Vec<_>>(), "{rows} rows, {degree}");
}
}
}
#[test]
fn a_run_that_opens_with_an_invalid_key_keeps_its_validity() {
let mut run = Run::default();
run.push(Record { user: 10, group: 0, pair_hash: 5 }, false);
run.push(Record { user: 11, group: 4, pair_hash: 5 }, true);
assert_eq!(run.validity, [false, true]);
assert!(!run.valid_at(0));
assert!(run.valid_at(1));
}
#[test]
fn a_full_run_throws_away_the_pairs_it_already_has() {
let mut run = Run::default();
for round in 0..(COMPACT_FROM * 4) {
let user = (round % 100) as i64;
let valid = round % 3 != 0;
let seed = group_seed(7, valid);
let pair_hash = folded(spread(mix(seed, user as u64)));
run.push(Record { user, group: 7, pair_hash }, valid);
}
assert!(run.rows.capacity() <= COMPACT_FROM * 2, "grew to {}", run.rows.capacity());
let mut partition = Held { runs: vec![run] };
let counted = distinct_pairs(&mut partition, 1, &rudb_common::Memory::unlimited())
.expect("a pair partition");
assert_eq!(counted.splits[0].len(), 200);
let nulls = counted.splits[0].iter().filter(|pair| !pair.valid).count();
assert_eq!(nulls, 100);
}
#[test]
fn a_null_key_does_not_join_the_group_whose_key_is_zero() {
let mut run = Run::default();
run.push(Record { user: 10, group: 0, pair_hash: 5 }, false);
run.push(Record { user: 10, group: 0, pair_hash: 5 }, true);
let mut partition = Held { runs: vec![run] };
let counted = distinct_pairs(&mut partition, 1, &rudb_common::Memory::unlimited())
.expect("a pair partition");
let mut found: Vec<bool> = counted.splits[0].iter().map(|pair| pair.valid).collect();
found.sort_unstable();
assert_eq!(found, [false, true]);
}
#[test]
fn scattering_from_a_seed_puts_a_row_where_scattering_from_the_group_would() {
for group in [i32::MIN, -7, 0, 1, 4096, i32::MAX] {
for valid in [true, false] {
assert_eq!(group_hash(group, valid), folded(group_seed(group, valid)));
for user in [i64::MIN, -1, 0, 99, i64::MAX] {
let mut plain: Vec<Run> = (0..PARTITIONS).map(|_| Run::default()).collect();
let mut seeded: Vec<Run> = (0..PARTITIONS).map(|_| Run::default()).collect();
scatter(&mut plain, shift(), group, valid, user);
scatter_seeded(
&mut seeded,
shift(),
group_seed(group, valid),
group,
valid,
user,
);
let at = |runs: &[Run]| {
runs.iter().position(|run| !run.rows.is_empty()).expect("a row landed")
};
let left = at(&plain);
assert_eq!(left, at(&seeded), "{group} {valid} {user}");
assert_eq!(plain[left].rows[0].pair_hash, seeded[left].rows[0].pair_hash);
assert_eq!(plain[left].validity, seeded[left].validity);
}
}
}
}
}