use std::sync::atomic::{AtomicUsize, Ordering};
#[inline]
pub fn default_workers() -> usize {
std::thread::available_parallelism().map(|x| x.get()).unwrap_or(1).max(1)
}
#[inline]
fn resolve_workers(requested: usize, units: usize) -> usize {
let w = if requested == 0 { default_workers() } else { requested };
w.max(1).min(units.max(1))
}
pub fn gatling_for_each<T, F>(n: usize, n_workers: usize, f: F) -> Vec<T>
where
T: Send,
F: Fn(usize) -> T + Sync,
{
if n == 0 {
return Vec::new();
}
let workers = resolve_workers(n_workers, n);
if workers == 1 {
return (0..n).map(f).collect();
}
let mut out: Vec<std::mem::MaybeUninit<T>> = Vec::with_capacity(n);
out.resize_with(n, std::mem::MaybeUninit::uninit);
let next = AtomicUsize::new(0);
{
let next_ref = &next;
let f_ref = &f;
let out_ptr = OutPtr(out.as_mut_ptr());
std::thread::scope(|s| {
for _ in 0..workers {
let out_ptr = out_ptr;
s.spawn(move || {
let out_ptr = &out_ptr;
loop {
let i = next_ref.fetch_add(1, Ordering::Relaxed);
if i >= n {
break;
}
let v = f_ref(i);
unsafe { out_ptr.write_at(i, v) };
}
});
}
});
}
#[cfg(feature = "testmatrix")]
crate::functional_status(
"gatling_forkjoin",
"gatling_for_each",
true,
&format!("n={n} workers={workers} completed"),
);
let mut out = std::mem::ManuallyDrop::new(out);
unsafe { Vec::from_raw_parts(out.as_mut_ptr() as *mut T, out.len(), out.capacity()) }
}
struct OutPtr<T>(*mut std::mem::MaybeUninit<T>);
impl<T> Clone for OutPtr<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for OutPtr<T> {}
unsafe impl<T: Send> Send for OutPtr<T> {}
impl<T> OutPtr<T> {
#[inline]
unsafe fn write_at(&self, i: usize, v: T) {
unsafe { self.0.add(i).write(std::mem::MaybeUninit::new(v)) };
}
}
pub fn gatling_run<F>(n: usize, n_workers: usize, f: F)
where
F: Fn(usize) + Sync,
{
if n == 0 {
return;
}
let workers = resolve_workers(n_workers, n);
if workers == 1 {
(0..n).for_each(f);
return;
}
let next = AtomicUsize::new(0);
let next_ref = &next;
let f_ref = &f;
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(move || loop {
let i = next_ref.fetch_add(1, Ordering::Relaxed);
if i >= n {
break;
}
f_ref(i);
});
}
});
#[cfg(feature = "testmatrix")]
crate::functional_status(
"gatling_forkjoin",
"gatling_run",
true,
&format!("n={n} workers={workers} completed"),
);
}
pub fn gatling_for_each_balanced<T, F, W>(
n: usize,
n_workers: usize,
batch: usize,
weight: W,
f: F,
) -> Vec<T>
where
T: Send,
F: Fn(usize) -> T + Sync,
W: Fn(usize) -> u64,
{
if n == 0 {
return Vec::new();
}
let workers = resolve_workers(n_workers, n);
if workers == 1 {
return (0..n).map(f).collect();
}
let batch = batch.max(1);
let weights: Vec<u64> = (0..n).map(&weight).collect();
let mut schedule: Vec<usize> = (0..n).collect();
schedule.sort_by(|&a, &b| weights[b].cmp(&weights[a]));
let mut out: Vec<std::mem::MaybeUninit<T>> = Vec::with_capacity(n);
out.resize_with(n, std::mem::MaybeUninit::uninit);
let next = AtomicUsize::new(0);
{
let next_ref = &next;
let f_ref = &f;
let sched_ref = &schedule;
let out_ptr = OutPtr(out.as_mut_ptr());
std::thread::scope(|s| {
for _ in 0..workers {
let out_ptr = out_ptr;
s.spawn(move || {
let out_ptr = &out_ptr;
loop {
let start = next_ref.fetch_add(batch, Ordering::Relaxed);
if start >= n {
break;
}
let end = (start + batch).min(n);
for &orig in &sched_ref[start..end] {
let v = f_ref(orig);
unsafe { out_ptr.write_at(orig, v) };
}
}
});
}
});
}
let mut out = std::mem::ManuallyDrop::new(out);
unsafe { Vec::from_raw_parts(out.as_mut_ptr() as *mut T, out.len(), out.capacity()) }
}
pub fn gatling_map_balanced<T, R, S, F>(
items: &[T],
n_workers: usize,
batch: usize,
size_of: S,
f: F,
) -> Vec<R>
where
T: Sync,
R: Send,
S: Fn(&T) -> u64,
F: Fn(usize, &T) -> R + Sync,
{
let n = items.len();
if n == 0 {
return Vec::new();
}
let workers = resolve_workers(n_workers, n);
if workers == 1 {
return items.iter().enumerate().map(|(i, t)| f(i, t)).collect();
}
let batch = batch.max(1);
let weights: Vec<u64> = items.iter().map(&size_of).collect();
let mut schedule: Vec<usize> = (0..n).collect();
schedule.sort_by(|&a, &b| weights[b].cmp(&weights[a]));
let mut out: Vec<std::mem::MaybeUninit<R>> = Vec::with_capacity(n);
out.resize_with(n, std::mem::MaybeUninit::uninit);
let next = AtomicUsize::new(0);
{
let next_ref = &next;
let f_ref = &f;
let sched_ref = &schedule;
let items_ref = items;
let out_ptr = OutPtr(out.as_mut_ptr());
std::thread::scope(|s| {
for _ in 0..workers {
let out_ptr = out_ptr;
s.spawn(move || {
let out_ptr = &out_ptr;
loop {
let start = next_ref.fetch_add(batch, Ordering::Relaxed);
if start >= n {
break;
}
let end = (start + batch).min(n);
for &orig in &sched_ref[start..end] {
let v = f_ref(orig, &items_ref[orig]);
unsafe { out_ptr.write_at(orig, v) };
}
}
});
}
});
}
let mut out = std::mem::ManuallyDrop::new(out);
unsafe { Vec::from_raw_parts(out.as_mut_ptr() as *mut R, out.len(), out.capacity()) }
}
pub fn gatling_map_owned<T, R, F>(items: Vec<T>, f: F) -> Vec<R>
where
T: Send,
R: Send,
F: Fn(T) -> R + Sync,
{
let n = items.len();
if n == 0 {
return Vec::new();
}
let workers = std::thread::available_parallelism()
.map(|w| w.get())
.unwrap_or(1)
.min(n);
if workers <= 1 {
return items.into_iter().map(f).collect();
}
use std::cell::UnsafeCell;
use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicUsize, Ordering};
struct Slots<X>(Vec<UnsafeCell<MaybeUninit<X>>>);
unsafe impl<X: Send> Sync for Slots<X> {}
let inputs = Slots(
items
.into_iter()
.map(|t| UnsafeCell::new(MaybeUninit::new(t)))
.collect::<Vec<_>>(),
);
let outputs: Slots<R> =
Slots((0..n).map(|_| UnsafeCell::new(MaybeUninit::uninit())).collect());
let cursor = AtomicUsize::new(0);
let inputs_ref = &inputs;
let outputs_ref = &outputs;
let cursor_ref = &cursor;
let f_ref = &f;
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(move || loop {
let i = cursor_ref.fetch_add(1, Ordering::Relaxed);
if i >= n {
break;
}
let item = unsafe { (*inputs_ref.0[i].get()).assume_init_read() };
let r = f_ref(item);
unsafe { (*outputs_ref.0[i].get()).write(r) };
});
}
});
outputs
.0
.into_iter()
.map(|c| unsafe { c.into_inner().assume_init() })
.collect()
}
pub fn gatling_scanlines<P, F>(
buf: &mut [P],
rows: usize,
stride: usize,
n_workers: usize,
min_rows_for_threads: usize,
f: F,
) where
P: Send,
F: Fn(usize, &mut [P]) + Sync,
{
debug_assert_eq!(buf.len(), rows * stride, "buf must be rows*stride");
if rows == 0 || stride == 0 {
return;
}
let workers = resolve_workers(n_workers, rows);
if workers == 1 || rows < min_rows_for_threads {
for (y, row) in buf.chunks_mut(stride).enumerate() {
f(y, row);
}
return;
}
let next = AtomicUsize::new(0);
let next_ref = &next;
let f_ref = &f;
let base = RowBase(buf.as_mut_ptr());
std::thread::scope(|s| {
for _ in 0..workers {
let base = base;
s.spawn(move || {
let base = &base; loop {
let y = next_ref.fetch_add(1, Ordering::Relaxed);
if y >= rows {
break;
}
let row = unsafe { base.row(y, stride) };
f_ref(y, row);
}
});
}
});
}
struct RowBase<P>(*mut P);
impl<P> Clone for RowBase<P> {
fn clone(&self) -> Self {
*self
}
}
impl<P> Copy for RowBase<P> {}
unsafe impl<P: Send> Send for RowBase<P> {}
impl<P> RowBase<P> {
#[inline]
unsafe fn row<'a>(&self, y: usize, stride: usize) -> &'a mut [P] {
unsafe { std::slice::from_raw_parts_mut(self.0.add(y * stride), stride) }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn for_each_is_ordered_and_complete() {
for &workers in &[0usize, 1, 2, 8] {
let n = 10_000;
let out = gatling_for_each(n, workers, |i| i * 3);
assert_eq!(out.len(), n);
for (i, v) in out.iter().enumerate() {
assert_eq!(*v, i * 3, "unit {i} wrong (workers={workers})");
}
}
}
#[test]
fn balanced_is_ordered_and_complete() {
for &workers in &[0usize, 1, 2, 8] {
for &batch in &[1usize, 4, 32] {
let n = 5_000;
let out = gatling_for_each_balanced(
n,
workers,
batch,
|i| if i % 500 == 0 { 1_000_000 } else { 1 },
|i| i * 3,
);
assert_eq!(out.len(), n);
for (i, v) in out.iter().enumerate() {
assert_eq!(*v, i * 3, "unit {i} wrong (workers={workers}, batch={batch})");
}
}
}
}
#[test]
fn map_balanced_is_ordered_complete_and_item_ref() {
for &workers in &[0usize, 1, 2, 8] {
for &batch in &[1usize, 4, 32] {
let n = 5_000usize;
let items: Vec<u64> = (0..n as u64)
.map(|i| if i % 500 == 0 { 1_000_000 } else { 1 })
.collect();
let out = gatling_map_balanced(
&items,
workers,
batch,
|&w| w, |i, &w| (i, w), );
assert_eq!(out.len(), n);
for (i, &(gi, gw)) in out.iter().enumerate() {
assert_eq!(gi, i, "item {i} out of order (workers={workers}, batch={batch})");
assert_eq!(gw, items[i], "item {i} got wrong &T");
}
}
}
let empty: Vec<u64> = Vec::new();
assert!(gatling_map_balanced(&empty, 8, 4, |&w| w, |i, &w| (i, w)).is_empty());
}
#[test]
fn balanced_batch_zero_and_single_unit() {
let out = gatling_for_each_balanced(1, 8, 0, |_| 1, |i| i + 7);
assert_eq!(out, vec![7]);
let out = gatling_for_each_balanced(3, 4, 0, |i| i as u64, |i| i * 10);
assert_eq!(out, vec![0, 10, 20]);
}
#[test]
fn run_visits_each_index_exactly_once() {
let n = 50_000;
let hits: Vec<AtomicUsize> = (0..n).map(|_| AtomicUsize::new(0)).collect();
gatling_run(n, 0, |i| {
hits[i].fetch_add(1, Ordering::Relaxed);
});
for (i, h) in hits.iter().enumerate() {
assert_eq!(h.load(Ordering::Relaxed), 1, "index {i} not visited exactly once");
}
}
#[test]
fn scanlines_fill_disjoint_rows_matching_sequential() {
let (rows, stride) = (300usize, 256usize);
let fill = |y: usize, row: &mut [u32]| {
for (x, p) in row.iter_mut().enumerate() {
*p = (y as u32) << 16 | x as u32;
}
};
let mut par = vec![0u32; rows * stride];
gatling_scanlines(&mut par, rows, stride, 0, 1, |y, row| fill(y, row));
let mut seq = vec![0u32; rows * stride];
for (y, row) in seq.chunks_mut(stride).enumerate() {
fill(y, row);
}
assert_eq!(par, seq, "parallel scanline raster matches sequential");
assert_eq!(par[42 * stride + 7], (42u32 << 16) | 7);
}
#[test]
fn scanlines_small_frame_stays_correct() {
let (rows, stride) = (4usize, 8usize);
let mut buf = vec![0u8; rows * stride];
gatling_scanlines(&mut buf, rows, stride, 0, 1024, |y, row| {
row.iter_mut().for_each(|p| *p = y as u8);
});
for y in 0..rows {
for x in 0..stride {
assert_eq!(buf[y * stride + x], y as u8);
}
}
}
#[test]
fn map_owned_equals_serial_map_all_sizes() {
for &n in &[0usize, 1, 7, 10_000] {
let v: Vec<u64> = (0..n as u64).map(|i| i.wrapping_mul(2654435761)).collect();
let f = |x: u64| x.wrapping_add(123).rotate_left(7);
let got = gatling_map_owned(v.clone(), f);
let want: Vec<u64> = v.into_iter().map(f).collect();
assert_eq!(got, want, "u64 map mismatch at n={n}");
let s: Vec<String> = (0..n).map(|i| format!("item-{i}")).collect();
let g = |mut x: String| {
x.push_str("!");
x
};
let got_s = gatling_map_owned(s.clone(), g);
let want_s: Vec<String> = s.into_iter().map(g).collect();
assert_eq!(got_s, want_s, "String map mismatch at n={n}");
}
}
#[test]
fn empty_inputs_are_noops() {
assert!(gatling_for_each(0, 4, |i| i).is_empty());
gatling_run(0, 4, |_| panic!("should not run"));
let mut empty: Vec<u8> = Vec::new();
gatling_scanlines(&mut empty, 0, 0, 4, 1, |_, _| panic!("should not run"));
}
}