use std::alloc::{GlobalAlloc, Layout, System};
use std::hint::black_box;
use std::sync::atomic::{AtomicUsize, Ordering};
use truecalc_core::eval::functions::lookup::index_match::index_fn;
use truecalc_core::Value;
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn allocations_during<T>(body: impl FnOnce() -> T) -> usize {
let before = ALLOCATIONS.load(Ordering::Relaxed);
black_box(body());
ALLOCATIONS.load(Ordering::Relaxed) - before
}
fn row_vector(n: usize) -> Value {
Value::Array((1..=n).map(|i| Value::Number(i as f64)).collect())
}
fn grid(rows: usize) -> Value {
Value::Array(
(1..=rows)
.map(|r| {
Value::Array(
(1..=3)
.map(|c| Value::Number((r * 10 + c) as f64))
.collect(),
)
})
.collect(),
)
}
fn column_vector(n: usize) -> Value {
Value::Array(
(1..=n)
.map(|i| Value::Array(vec![Value::Number(i as f64)]))
.collect(),
)
}
const SIZES: [usize; 5] = [100, 500, 1_000, 5_000, 10_000];
fn allocations_for_one_lookup(array: Value, row: usize, col: Option<usize>) -> usize {
let mut args = vec![array, Value::Number(row as f64)];
if let Some(c) = col {
args.push(Value::Number(c as f64));
}
black_box(index_fn(&args));
allocations_during(|| index_fn(&args))
}
#[test]
fn index_into_an_inline_array_allocates_a_constant_amount() {
let row_counts: Vec<(usize, usize)> = SIZES
.iter()
.map(|&n| (n, allocations_for_one_lookup(row_vector(n), n / 2, None)))
.collect();
let grid_counts: Vec<(usize, usize)> = SIZES
.iter()
.map(|&n| (n, allocations_for_one_lookup(grid(n), n / 2, Some(2))))
.collect();
let column_counts: Vec<(usize, usize)> = SIZES
.iter()
.map(|&n| {
(
n,
allocations_for_one_lookup(column_vector(n), n / 2, Some(1)),
)
})
.collect();
let report = |label: &str, counts: &[(usize, usize)]| {
let cells: Vec<String> = counts.iter().map(|(n, a)| format!("{n}: {a}")).collect();
format!(
"{label} — allocations per INDEX call by array size: {}",
cells.join(", ")
)
};
let measured = [
("row vector", &row_counts),
("grid", &grid_counts),
("column vector", &column_counts),
];
for (label, counts) in measured {
println!("{}", report(label, counts));
}
for (label, counts) in measured {
let first = counts[0].1;
for &(n, allocs) in counts {
assert_eq!(
allocs,
first,
"{label}: INDEX allocated {allocs} times at size {n} but {first} at size {}. \
The cost of reading one element must not depend on how big the array is. \
{}",
counts[0].0,
report(label, counts),
);
}
assert_eq!(
first,
0,
"{label}: reading one element out of an array must not allocate. {}",
report(label, counts),
);
}
let pass_counts: Vec<(usize, usize)> = SIZES
.iter()
.map(|&n| {
let array = grid(n);
let mut args = vec![array, Value::Number(1.0), Value::Number(2.0)];
black_box(index_fn(&args));
let allocs = allocations_during(|| {
for row in 1..=n {
args[1] = Value::Number(row as f64);
black_box(index_fn(&args));
}
});
(n, allocs)
})
.collect();
let pass_report: Vec<String> = pass_counts
.iter()
.map(|(n, a)| format!("{n} rows: {a}"))
.collect();
println!(
"one lookup per row — total allocations for the whole pass: {}",
pass_report.join(", ")
);
for &(n, allocs) in &pass_counts {
assert_eq!(
allocs,
0,
"reading all {n} rows of an inline array allocated {allocs} times; \
it must not allocate at all. {}",
pass_report.join(", "),
);
}
}