const SMALL_LEN: usize = 255;
const MEDIUM_LEN: usize = 1 << 20;
const TILE: usize = 16;
const NBR_SEGMENTS: usize = 4;
const SEGMENT: usize = TILE / NBR_SEGMENTS;
const RECURSIVE_LIMIT: usize = 128;
#[cfg(feature = "parallel")]
const PARALLEL_THRESHOLD: usize = 4 << 20;
#[inline]
pub(crate) fn transpose<T: Copy + Send + Sync>(
input: &[T],
output: &mut [T],
width: usize,
height: usize,
) {
assert_eq!(
input.len(),
width * height,
"input length {} != width*height {}",
input.len(),
width * height
);
assert_eq!(
output.len(),
width * height,
"output length {} != width*height {}",
output.len(),
width * height
);
if width == 0 || height == 0 {
return;
}
let len = width * height;
#[cfg(feature = "parallel")]
if len >= PARALLEL_THRESHOLD {
transpose_parallel(input, output, width, height);
return;
}
if len <= SMALL_LEN {
transpose_small(input, output, width, height);
return;
}
unsafe {
if len <= MEDIUM_LEN {
transpose_region::<false, T>(input, output, 0, height, 0, width, width, height, 0);
} else {
transpose_recursive(input, output, 0, height, 0, width, width, height, 0);
}
}
}
#[inline]
fn transpose_small<T: Copy>(input: &[T], output: &mut [T], width: usize, height: usize) {
for x in 0..width {
for y in 0..height {
unsafe {
*output.get_unchecked_mut(x * height + y) = *input.get_unchecked(x + y * width);
}
}
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
unsafe fn transpose_block<T: Copy>(
input: &[T],
output: &mut [T],
total_cols: usize,
total_rows: usize,
start_x: usize,
start_y: usize,
out_col_start: usize,
block_width: usize,
block_height: usize,
) {
unsafe {
for inner_x in 0..block_width {
let x = start_x + inner_x;
let out_base = (out_col_start + inner_x) * total_rows + start_y;
for inner_y in 0..block_height {
*output.get_unchecked_mut(out_base + inner_y) =
*input.get_unchecked(x + (start_y + inner_y) * total_cols);
}
}
}
}
#[inline]
unsafe fn transpose_tile_plain<T: Copy>(
input: &[T],
output: &mut [T],
total_cols: usize,
total_rows: usize,
start_x: usize,
start_y: usize,
out_col_start: usize,
) {
unsafe {
for inner_x in 0..TILE {
let x = start_x + inner_x;
let out_base = (out_col_start + inner_x) * total_rows + start_y;
for inner_y in 0..TILE {
*output.get_unchecked_mut(out_base + inner_y) =
*input.get_unchecked(x + (start_y + inner_y) * total_cols);
}
}
}
}
#[inline]
unsafe fn transpose_tile_segmented<T: Copy>(
input: &[T],
output: &mut [T],
total_cols: usize,
total_rows: usize,
start_x: usize,
start_y: usize,
out_col_start: usize,
) {
unsafe {
for segment in 0..NBR_SEGMENTS {
let seg_y = start_y + segment * SEGMENT;
for inner_x in 0..TILE {
let x = start_x + inner_x;
let out_base = (out_col_start + inner_x) * total_rows + seg_y;
for inner_y in 0..SEGMENT {
*output.get_unchecked_mut(out_base + inner_y) =
*input.get_unchecked(x + (seg_y + inner_y) * total_cols);
}
}
}
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
unsafe fn transpose_region<const SEGMENTED: bool, T: Copy>(
input: &[T],
output: &mut [T],
row_start: usize,
row_end: usize,
col_start: usize,
col_end: usize,
total_cols: usize,
total_rows: usize,
col_out_base: usize,
) {
let nbr_rows = row_end - row_start;
let nbr_cols = col_end - col_start;
let x_tiles = nbr_cols / TILE;
let y_tiles = nbr_rows / TILE;
let remainder_x = nbr_cols - x_tiles * TILE;
let remainder_y = nbr_rows - y_tiles * TILE;
let out_col_start = col_start - col_out_base;
unsafe {
for y_tile in 0..y_tiles {
let row = row_start + y_tile * TILE;
for x_tile in 0..x_tiles {
let col = col_start + x_tile * TILE;
let out_col = out_col_start + x_tile * TILE;
if SEGMENTED {
transpose_tile_segmented(
input, output, total_cols, total_rows, col, row, out_col,
);
} else {
transpose_tile_plain(input, output, total_cols, total_rows, col, row, out_col);
}
}
if remainder_x > 0 {
transpose_block(
input,
output,
total_cols,
total_rows,
col_start + x_tiles * TILE,
row,
out_col_start + x_tiles * TILE,
remainder_x,
TILE,
);
}
}
if remainder_y > 0 {
let row = row_start + y_tiles * TILE;
for x_tile in 0..x_tiles {
transpose_block(
input,
output,
total_cols,
total_rows,
col_start + x_tile * TILE,
row,
out_col_start + x_tile * TILE,
TILE,
remainder_y,
);
}
if remainder_x > 0 {
transpose_block(
input,
output,
total_cols,
total_rows,
col_start + x_tiles * TILE,
row,
out_col_start + x_tiles * TILE,
remainder_x,
remainder_y,
);
}
}
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
unsafe fn transpose_recursive<T: Copy>(
input: &[T],
output: &mut [T],
row_start: usize,
row_end: usize,
col_start: usize,
col_end: usize,
total_cols: usize,
total_rows: usize,
col_out_base: usize,
) {
let nbr_rows = row_end - row_start;
let nbr_cols = col_end - col_start;
unsafe {
if (nbr_rows <= RECURSIVE_LIMIT && nbr_cols <= RECURSIVE_LIMIT)
|| nbr_rows <= 2
|| nbr_cols <= 2
{
transpose_region::<true, T>(
input,
output,
row_start,
row_end,
col_start,
col_end,
total_cols,
total_rows,
col_out_base,
);
return;
}
if nbr_rows >= nbr_cols {
let mid = row_start + nbr_rows / 2;
transpose_recursive(
input,
output,
row_start,
mid,
col_start,
col_end,
total_cols,
total_rows,
col_out_base,
);
transpose_recursive(
input,
output,
mid,
row_end,
col_start,
col_end,
total_cols,
total_rows,
col_out_base,
);
} else {
let mid = col_start + nbr_cols / 2;
transpose_recursive(
input,
output,
row_start,
row_end,
col_start,
mid,
total_cols,
total_rows,
col_out_base,
);
transpose_recursive(
input,
output,
row_start,
row_end,
mid,
col_end,
total_cols,
total_rows,
col_out_base,
);
}
}
}
#[cfg(feature = "parallel")]
fn transpose_parallel<T: Copy + Send + Sync>(
input: &[T],
output: &mut [T],
width: usize,
height: usize,
) {
use rayon::prelude::*;
let num_threads = rayon::current_num_threads();
let cols_per_task = width.div_ceil(num_threads).max(1);
output
.par_chunks_mut(cols_per_task * height)
.enumerate()
.for_each(|(task, band)| {
let col_start = task * cols_per_task;
let col_end = (col_start + cols_per_task).min(width);
unsafe {
transpose_recursive(
input, band, 0, height, col_start, col_end, width, height, col_start,
);
}
});
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use proptest::prelude::*;
use super::*;
fn reference<T: Copy>(input: &[T], width: usize, height: usize, zero: T) -> Vec<T> {
let mut out = vec![zero; width * height];
for y in 0..height {
for x in 0..width {
out[x * height + y] = input[y * width + x];
}
}
out
}
fn check<T>(width: usize, height: usize, zero: T, make: impl Fn(usize) -> T)
where
T: Copy + PartialEq + core::fmt::Debug + Send + Sync,
{
let input: Vec<T> = (0..width * height).map(&make).collect();
let mut output = vec![zero; width * height];
transpose(&input, &mut output, width, height);
let expected = reference(&input, width, height, zero);
assert_eq!(output, expected, "mismatch for {width}x{height}");
}
fn dims() -> impl Strategy<Value = (usize, usize)> {
prop_oneof![
Just((0, 0)),
Just((1, 1)),
(1..=200_usize).prop_map(|w| (w, 1)),
(1..=200_usize).prop_map(|h| (1, h)),
(1..=15_usize, 1..=15_usize),
((TILE - 1)..=(TILE + 1), (TILE - 1)..=(TILE + 1)),
Just((TILE * 4, TILE * 4)),
((TILE + 1)..=(TILE * 6), (TILE + 1)..=(TILE * 6)),
(120..=140_usize, 120..=140_usize),
(40..=300_usize, 10..=60_usize),
(10..=60_usize, 40..=300_usize),
Just((1024, 1024)),
Just((1025, 1024)), Just((1024, 1025)), Just((1100, 1000)), Just((2003, 757)), Just((757, 2003)), ]
}
proptest! {
#[test]
fn portable_matches_reference_u32((width, height) in dims()) {
check(width, height, 0u32, |i| i as u32);
}
#[test]
fn portable_matches_reference_u64((width, height) in dims()) {
check(width, height, 0u64, |i| i as u64);
}
}
fn dims_small() -> impl Strategy<Value = (usize, usize)> {
prop_oneof![
Just((0, 0)),
Just((1, 1)),
(1..=200_usize).prop_map(|w| (w, 1)),
(1..=200_usize).prop_map(|h| (1, h)),
(1..=15_usize, 1..=15_usize),
((TILE - 1)..=(TILE + 1), (TILE - 1)..=(TILE + 1)),
((TILE + 1)..=(TILE * 6), (TILE + 1)..=(TILE * 6)),
(40..=300_usize, 10..=60_usize),
(10..=60_usize, 40..=300_usize),
Just((1025, 1024)),
]
}
proptest! {
#[test]
fn portable_matches_reference_u8((width, height) in dims_small()) {
check(width, height, 0u8, |i| i as u8);
}
#[test]
fn portable_matches_reference_u128((width, height) in dims_small()) {
check(width, height, 0u128, |i| i as u128);
}
#[test]
fn portable_matches_reference_3byte((width, height) in dims_small()) {
check(width, height, [0u8; 3], |i| [i as u8, (i >> 8) as u8, (i >> 16) as u8]);
}
#[test]
fn portable_matches_reference_32byte((width, height) in dims_small()) {
check(width, height, [0u64; 4], |i| [i as u64, 0, 0, i as u64]);
}
}
#[cfg(feature = "parallel")]
#[test]
fn portable_parallel_path() {
for &(w, h) in &[(2050, 2050), (4097, 1025), (1025, 4097), (3, 1_400_001)] {
check(w, h, 0u32, |i| i as u32);
}
}
}