#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[inline]
fn fxhash(mut x: u64) -> u64 {
const K: u64 = 0x517cc1b727220a95;
x = x.wrapping_mul(K);
x ^= x >> 33;
x = x.wrapping_mul(K);
x ^= x >> 29;
x
}
#[inline]
fn fxhash_combine(seed: u64, index: u64) -> u64 {
fxhash(seed ^ fxhash(index))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartitionMode {
EqualSize,
WorkStealing,
}
pub struct LanePartitioner {
pub num_lanes: usize,
pub mode: PartitionMode,
}
impl LanePartitioner {
pub fn new(num_lanes: usize, mode: PartitionMode) -> Self {
let num_lanes = num_lanes.max(1);
Self { num_lanes, mode }
}
#[cfg(feature = "std")]
pub fn auto(mode: PartitionMode) -> Self {
let num_lanes = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
Self::new(num_lanes, mode)
}
pub fn partition(&self, num_items: usize, seed: u64) -> Vec<Vec<usize>> {
partition(num_items, self.num_lanes, seed, self.mode)
}
}
pub fn partition(
num_items: usize,
num_lanes: usize,
seed: u64,
mode: PartitionMode,
) -> Vec<Vec<usize>> {
let num_lanes = num_lanes.max(1);
if num_items == 0 {
return vec![Vec::new(); num_lanes];
}
let base_per_lane = num_items / num_lanes;
let extra = num_items % num_lanes;
let capacity = base_per_lane + 1;
let mut lanes: Vec<Vec<usize>> = (0..num_lanes)
.map(|_| Vec::with_capacity(capacity))
.collect();
for idx in 0..num_items {
let h = fxhash_combine(seed, idx as u64);
let lane = (h % num_lanes as u64) as usize;
lanes[lane].push(idx);
}
if mode == PartitionMode::WorkStealing {
let all_items: Vec<usize> = lanes.drain(..).flat_map(|l| l.into_iter()).collect();
lanes.clear();
let mut start = 0;
for i in 0..num_lanes {
let target = if i < extra { base_per_lane + 1 } else { base_per_lane };
lanes.push(all_items[start..start + target].to_vec());
start += target;
}
}
lanes
}
pub fn partition_chunks(
num_chunks: usize,
num_lanes: usize,
seed: u64,
) -> Vec<Vec<usize>> {
partition(num_chunks, num_lanes, seed, PartitionMode::WorkStealing)
}
#[derive(Debug, Clone, Default)]
pub struct LaneStats {
pub chunks_processed: usize,
pub compressed_bytes: u64,
pub decompressed_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct PartitionStats {
pub per_lane: Vec<LaneStats>,
pub total_chunks: usize,
pub num_lanes: usize,
}
impl PartitionStats {
pub fn new(num_lanes: usize) -> Self {
Self {
per_lane: (0..num_lanes).map(|_| LaneStats::default()).collect(),
total_chunks: 0,
num_lanes,
}
}
pub fn imbalance(&self) -> (usize, usize) {
let max = self.per_lane.iter().map(|s| s.chunks_processed).max().unwrap_or(0);
let min = self.per_lane.iter().map(|s| s.chunks_processed).min().unwrap_or(0);
(max, min)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeSet as HashSet;
#[cfg(feature = "std")]
use std::collections::HashSet;
#[test]
fn all_items_covered_no_duplicates() {
for n in [0, 1, 2, 5, 10, 16, 31, 100] {
for lanes in [1, 2, 4, 8, 16] {
let result = partition(n, lanes, 42, PartitionMode::EqualSize);
assert_eq!(result.len(), lanes);
let mut seen = HashSet::new();
let mut total = 0;
for lane in &result {
for &idx in lane {
assert!(idx < n, "index {idx} out of range for n={n}");
assert!(seen.insert(idx), "duplicate index {idx}");
total += 1;
}
}
assert_eq!(total, n, "not all items covered for n={n}, lanes={lanes}");
}
}
}
#[test]
fn deterministic_same_seed() {
let a = partition(50, 4, 12345, PartitionMode::EqualSize);
let b = partition(50, 4, 12345, PartitionMode::EqualSize);
assert_eq!(a, b);
}
#[test]
fn different_seed_different_partition() {
let a = partition(50, 4, 100, PartitionMode::EqualSize);
let b = partition(50, 4, 200, PartitionMode::EqualSize);
assert_ne!(a, b);
}
#[test]
fn work_stealing_rebalances() {
for n in [7, 13, 31, 100] {
for lanes in [2, 4, 8, 16] {
let result = partition(n, lanes, 999, PartitionMode::WorkStealing);
let sizes: Vec<usize> = result.iter().map(|l| l.len()).collect();
let max = *sizes.iter().max().unwrap();
let min = *sizes.iter().min().unwrap();
assert!(
max - min <= 3,
"imbalance too high: max={max}, min={min} for n={n}, lanes={lanes}"
);
let mut seen = HashSet::new();
for lane in &result {
for &idx in lane {
assert!(seen.insert(idx));
}
}
assert_eq!(seen.len(), n);
}
}
}
#[test]
fn partition_chunks_convenience() {
let result = partition_chunks(20, 4, 42);
assert_eq!(result.len(), 4);
let total: usize = result.iter().map(|l| l.len()).sum();
assert_eq!(total, 20);
}
#[test]
fn single_lane() {
let result = partition(10, 1, 0, PartitionMode::EqualSize);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 10);
}
#[test]
fn zero_items() {
let result = partition(0, 4, 0, PartitionMode::EqualSize);
assert_eq!(result.len(), 4);
for lane in &result {
assert!(lane.is_empty());
}
}
#[test]
fn more_lanes_than_items() {
let result = partition(3, 16, 42, PartitionMode::WorkStealing);
assert_eq!(result.len(), 16);
let total: usize = result.iter().map(|l| l.len()).sum();
assert_eq!(total, 3);
}
#[test]
fn lane_partitioner_struct() {
let lp = LanePartitioner::new(4, PartitionMode::EqualSize);
let result = lp.partition(20, 42);
assert_eq!(result.len(), 4);
let total: usize = result.iter().map(|l| l.len()).sum();
assert_eq!(total, 20);
}
#[test]
fn partition_stats_imbalance() {
let mut stats = PartitionStats::new(4);
stats.per_lane[0].chunks_processed = 5;
stats.per_lane[1].chunks_processed = 5;
stats.per_lane[2].chunks_processed = 6;
stats.per_lane[3].chunks_processed = 4;
let (max, min) = stats.imbalance();
assert_eq!(max, 6);
assert_eq!(min, 4);
}
#[test]
fn fxhash_deterministic() {
assert_eq!(fxhash(42), fxhash(42));
assert_ne!(fxhash(1), fxhash(2));
}
#[test]
fn lane_count_various() {
for &lanes in &[1, 2, 4, 8, 16] {
let result = partition(32, lanes, 0xDEAD, PartitionMode::WorkStealing);
assert_eq!(result.len(), lanes);
let total: usize = result.iter().map(|l| l.len()).sum();
assert_eq!(total, 32);
let mut all: Vec<usize> = result.into_iter().flatten().collect();
all.sort();
let expected: Vec<usize> = (0..32).collect();
assert_eq!(all, expected);
}
}
}