use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
#[cfg(feature = "parallel")]
use std::sync::atomic::{AtomicUsize, Ordering};
use strided_perm::copy_into;
use strided_view::{StridedArray, StridedViewMut, INLINE_RANK};
use threadteam::Exec;
#[cfg(feature = "parallel")]
use threadteam::Pool;
struct CountingAllocator;
#[cfg(feature = "parallel")]
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
thread_local! {
static THREAD_ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
}
fn record_allocation() {
#[cfg(feature = "parallel")]
ALLOCATIONS.fetch_add(1, Ordering::SeqCst);
let _ = THREAD_ALLOCATIONS.try_with(|count| count.set(count.get() + 1));
}
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
record_allocation();
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
record_allocation();
System.realloc(ptr, layout, new_size)
}
}
#[global_allocator]
static GLOBAL: CountingAllocator = CountingAllocator;
fn count_thread_allocations(run: impl FnOnce()) -> usize {
let before = THREAD_ALLOCATIONS.with(Cell::get);
run();
THREAD_ALLOCATIONS.with(Cell::get) - before
}
#[cfg(feature = "parallel")]
fn count_process_allocations(run: impl FnOnce()) -> usize {
let before = ALLOCATIONS.load(Ordering::SeqCst);
run();
ALLOCATIONS.load(Ordering::SeqCst) - before
}
fn col_major_strides(dims: &[usize]) -> Vec<isize> {
let mut stride = 1isize;
dims.iter()
.map(|&dim| {
let current = stride;
stride *= dim as isize;
current
})
.collect()
}
fn max_offset(dims: &[usize], strides: &[isize]) -> isize {
dims.iter()
.zip(strides)
.map(|(&dim, &stride)| (dim as isize - 1) * stride)
.sum()
}
fn source_array(dims: &[usize], strides: &[isize]) -> StridedArray<f64> {
let data: Vec<f64> = (0..=(max_offset(dims, strides) as usize))
.map(|value| value as f64)
.collect();
StridedArray::from_parts(data, dims, strides, 0).unwrap()
}
fn grouped_rank8_strides() -> (Vec<usize>, Vec<isize>) {
let dims = vec![2usize; 8];
let src = vec![2isize, 1, 4, 8, 16, 32, 64, 128];
(dims, src)
}
fn serial_allocations(dims: &[usize], src_strides: &[isize], dst_strides: &[isize]) -> usize {
let src = source_array(dims, src_strides);
let mut dst_data = vec![0.0f64; max_offset(dims, dst_strides) as usize + 1];
let mut dst = StridedViewMut::new(&mut dst_data, dims, dst_strides, 0).unwrap();
let src_view = src.view();
count_thread_allocations(|| copy_into(&mut dst, &src_view, Exec::serial()).unwrap())
}
fn check_serial_contract() {
for rank in 1..=INLINE_RANK {
let dims = vec![2usize; rank];
let dst_strides = col_major_strides(&dims);
assert_eq!(
serial_allocations(&dims, &dst_strides, &dst_strides),
0,
"contiguous rank {rank}"
);
let mut permuted = dst_strides.clone();
permuted.reverse();
assert_eq!(
serial_allocations(&dims, &permuted, &dst_strides),
0,
"permuted rank {rank}"
);
if rank >= 2 {
let mut scattered = dst_strides.clone();
scattered.rotate_left(1);
assert_eq!(
serial_allocations(&dims, &scattered, &dst_strides),
0,
"scattered rank {rank}"
);
}
}
let (dims, src_strides) = grouped_rank8_strides();
let dst_strides = col_major_strides(&dims);
assert_eq!(
serial_allocations(&dims, &src_strides, &dst_strides),
0,
"grouped rank 8"
);
let dims9 = vec![2usize; INLINE_RANK + 1];
let mut strides9 = col_major_strides(&dims9);
strides9.reverse();
assert!(
serial_allocations(&dims9, &strides9, &col_major_strides(&dims9)) > 0,
"rank 9 must spill"
);
let dims = [100usize, 2, 2, 2, 2, 2, 2, 2];
let src_strides = [1isize, 101, 1000, 1001, 1002, 1003, 1004, 1005];
let dst_strides = [128isize, 1, 2, 4, 8, 16, 32, 64];
let allocations = serial_allocations(&dims, &src_strides, &dst_strides);
assert!(
allocations > 0,
"a 100-entry grouped table must spill, got {allocations}"
);
println!("spill case [100,2x7] allocations = {allocations}");
let dims = [2usize, 3, 4, 5, 6, 7, 8, 9];
let dst_strides = col_major_strides(&dims);
let mut src_strides = dst_strides.clone();
src_strides.swap(0, 7);
let allocations = serial_allocations(&dims, &src_strides, &dst_strides);
assert!(
allocations > 0,
"a 120-entry grouped table must spill, got {allocations}"
);
println!("spill case [2,3,..,9] swapped allocations = {allocations}");
}
#[cfg(feature = "parallel")]
fn check_parallel_contract() {
let pool = Pool::builder().threads(2).build().unwrap();
Exec::new(&pool, 2).unwrap().fork(2, |_, _| {});
std::thread::sleep(std::time::Duration::from_millis(100));
let pooled_extra = |dst: &mut StridedViewMut<f64>, src: &strided_view::StridedView<f64>| {
let one = Exec::new(&pool, 1).unwrap();
let two = Exec::new(&pool, 2).unwrap();
let inline = count_thread_allocations(|| copy_into(dst, src, one).unwrap());
let pooled = count_process_allocations(|| copy_into(dst, src, two).unwrap());
(inline, pooled)
};
for rank in 1..=INLINE_RANK {
let dims = vec![2usize; rank];
let dst_strides = col_major_strides(&dims);
let mut permuted = dst_strides.clone();
permuted.reverse();
let mut scattered = dst_strides.clone();
if rank >= 2 {
scattered.rotate_left(1);
}
for (label, src_strides) in [("permuted", permuted), ("scattered", scattered)] {
let src = source_array(&dims, &src_strides);
let mut dst_data = vec![0.0f64; max_offset(&dims, &dst_strides) as usize + 1];
let mut dst = StridedViewMut::new(&mut dst_data, &dims, &dst_strides, 0).unwrap();
let src_view = src.view();
copy_into(&mut dst, &src_view, Exec::new(&pool, 2).unwrap()).unwrap();
let (inline, pooled) = pooled_extra(&mut dst, &src_view);
assert_eq!(inline, 0, "inline {label} rank {rank}");
assert!(
pooled <= MAX_SCHEDULER_ALLOCATIONS,
"pooled {label} rank {rank}: {pooled} allocations"
);
}
}
let dims = [32usize, 32, 32];
let src_strides = [1024isize, 1, 32];
let src = source_array(&dims, &src_strides);
let mut dst_data = vec![0.0f64; max_offset(&dims, &col_major_strides(&dims)) as usize + 1];
let dst_strides = col_major_strides(&dims);
let mut dst = StridedViewMut::new(&mut dst_data, &dims, &dst_strides, 0).unwrap();
let src_view = src.view();
for _ in 0..4 {
copy_into(&mut dst, &src_view, Exec::new(&pool, 2).unwrap()).unwrap();
}
let (inline, pooled) = pooled_extra(&mut dst, &src_view);
assert_eq!(inline, 0, "inline copy at rank 3");
assert!(
pooled <= MAX_SCHEDULER_ALLOCATIONS,
"pooled copy at rank 3: {pooled} allocations"
);
let (dims8, src8) = grouped_rank8_strides();
let src8 = source_array(&dims8, &src8);
let dst8_strides = col_major_strides(&dims8);
let mut dst8_data = vec![0.0f64; max_offset(&dims8, &dst8_strides) as usize + 1];
let mut dst8 = StridedViewMut::new(&mut dst8_data, &dims8, &dst8_strides, 0).unwrap();
let src8_view = src8.view();
copy_into(&mut dst8, &src8_view, Exec::new(&pool, 2).unwrap()).unwrap();
let (inline, pooled) = pooled_extra(&mut dst8, &src8_view);
assert_eq!(inline, 0, "inline grouped rank-8 copy");
assert!(
pooled <= MAX_SCHEDULER_ALLOCATIONS,
"pooled grouped rank-8 copy: {pooled} allocations"
);
}
#[cfg(feature = "parallel")]
const MAX_SCHEDULER_ALLOCATIONS: usize = 16;
#[test]
fn copy_entries_are_allocation_free_within_the_contract() {
check_serial_contract();
#[cfg(feature = "parallel")]
check_parallel_contract();
}