p3_util/transpose/rectangular.rs
1//! High-performance matrix transpose for generic `Copy` types.
2//!
3//! This module provides an optimized **out-of-place** matrix transpose.
4//!
5//! # Overview
6//!
7//! Matrix transposition converts a row-major matrix into its column-major equivalent.
8//! For a matrix `A` with dimensions `height × width`:
9//!
10//! ```text
11//! A[i][j] → A^T[j][i]
12//! ```
13//!
14//! In memory (row-major layout), element at position `(row, col)` is stored at:
15//! - **Input**: `input[row * width + col]`
16//! - **Output**: `output[col * height + row]`
17//!
18//! # Architecture-Specific Optimizations
19//!
20//! On **ARM64** (aarch64), this module uses NEON SIMD intrinsics for:
21//! - **4-byte elements** (typical for 32-bit field elements like `MontyField31`, `BabyBear`)
22//! using a 2-stage butterfly (`vtrn1q_u32`/`vtrn2q_u32` then `vtrn1q_u64`/`vtrn2q_u64`).
23//! - **8-byte elements** (typical for 64-bit field elements like `Goldilocks`)
24//! using a simpler 1-stage butterfly (`vtrn1q_u64`/`vtrn2q_u64`) on pairs of registers.
25//!
26//! On other architectures, or for other element sizes, it falls back to the portable engine.
27//!
28//! # Key Optimizations
29//!
30//! ## NEON SIMD Registers (128-bit)
31//!
32//! ARM64 NEON provides 32 vector registers, each holding 128 bits.
33//! - For 32-bit (4-byte) elements, each register holds exactly **`BLOCK_SIZE` elements**.
34//! - A `BLOCK_SIZE`×`BLOCK_SIZE` block (`BLOCK_SIZE`^2 elements) fits perfectly in **`BLOCK_SIZE` registers**.
35//!
36//! ```text
37//! ┌─────────────────────────────────┐
38//! │ q0 = [ a00, a01, a02, a03 ] │ ← 128 bits = 4 × 32-bit
39//! │ q1 = [ a10, a11, a12, a13 ] │
40//! │ q2 = [ a20, a21, a22, a23 ] │
41//! │ q3 = [ a30, a31, a32, a33 ] │
42//! └─────────────────────────────────┘
43//! ```
44//!
45//! ## In-Register Transpose (Butterfly Network)
46//!
47//! We transpose a `BLOCK_SIZE`×`BLOCK_SIZE` block entirely in registers using a 2-stage butterfly:
48//!
49//! **Stage 1**: Swap pairs of 32-bit elements using `TRN1`/`TRN2`
50//! **Stage 2**: Swap pairs of 64-bit elements using `TRN1`/`TRN2` on reinterpreted u64
51//!
52//! ```text
53//! Input: After Stage 1: After Stage 2 (Output):
54//! ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
55//! │ a b │ c d │ │ a e │ c g │ │ a e │ i m │
56//! │ e f │ g h │ │ b f │ d h │ │ b f │ j n │
57//! │─────┼───────│ │─────┼───────│ │─────┼───────│
58//! │ i j │ k l │ │ i m │ k o │ │ c g │ k o │
59//! │ m n │ o p │ │ j n │ l p │ │ d h │ l p │
60//! └─────────────┘ └─────────────┘ └─────────────┘
61//! ```
62//!
63//! ## Multi-Level Tiling Strategy
64//!
65//! Different strategies for different matrix sizes:
66//! - **Small (<`SMALL_LEN` elements)**: Scalar transpose - fits in L1, no overhead
67//! - **Medium (<`MEDIUM_LEN` elements)**: `TILE_SIZE`×`TILE_SIZE` Tiled - L2-friendly tiles
68//! - **Large (≥`MEDIUM_LEN` elements)**: Recursive + Tiled - Cache-oblivious
69
70#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
71use core::arch::aarch64::*;
72#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
73use core::mem::MaybeUninit;
74#[cfg(all(target_arch = "aarch64", feature = "parallel"))]
75use core::sync::atomic::{AtomicUsize, Ordering};
76
77/// Software prefetch for write (PRFM PSTL1KEEP).
78///
79/// Brings the cache line containing `ptr` into the L1 data cache in exclusive
80/// state, preparing for a subsequent store. This avoids Read-For-Ownership
81/// (RFO) stalls when writing to memory not already in L1.
82#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
83#[inline(always)]
84unsafe fn prefetch_write(ptr: *const u8) {
85 // PRFM PSTL1KEEP: Prefetch for Store, L1 cache, temporal (keep in cache).
86 unsafe {
87 core::arch::asm!(
88 "prfm pstl1keep, [{ptr}]",
89 ptr = in(reg) ptr,
90 options(readonly, nostack, preserves_flags),
91 );
92 }
93}
94
95/// Maximum number of elements for the simple scalar transpose.
96///
97/// For matrices with fewer than `SMALL_LEN` elements (~1KB for 4-byte elements),
98/// the overhead of tiling isn't worth it.
99///
100/// Direct element-by-element copy is faster because:
101/// - The entire matrix fits in L1 cache (32-64KB on most CPUs)
102/// - No tile boundary calculations needed
103/// - Branch prediction works well for small loops
104#[cfg(any(target_arch = "aarch64", test))]
105const SMALL_LEN: usize = 255;
106
107/// Maximum number of elements for the single-level tiled transpose.
108///
109/// For matrices up to `MEDIUM_LEN` elements (~4MB for 4-byte elements), we use
110/// a simple tiled approach with `TILE_SIZE`×`TILE_SIZE` tiles.
111///
112/// This fits comfortably within L2 cache (256KB-512KB) with good spatial locality.
113///
114/// Beyond this threshold, we switch to recursive subdivision to ensure
115/// cache-oblivious behavior for very large matrices.
116#[cfg(any(target_arch = "aarch64", test))]
117const MEDIUM_LEN: usize = 1024 * 1024;
118
119/// Side length of a tile in elements.
120///
121/// We use `TILE_SIZE`×`TILE_SIZE` tiles because:
122/// - `TILE_SIZE`×`TILE_SIZE` × 4 bytes = 1KB per tile, fitting in L1 cache
123/// - `TILE_SIZE` is divisible by `BLOCK_SIZE`, allowing exactly (`TILE_SIZE`/`BLOCK_SIZE`)^2 NEON blocks per tile
124/// - Good balance between tile overhead and cache utilization
125#[cfg(any(target_arch = "aarch64", test))]
126const TILE_SIZE: usize = 16;
127
128/// Maximum dimension for recursive base case.
129///
130/// When recursively subdividing large matrices, we stop when both
131/// dimensions are ≤ `RECURSIVE_LIMIT` elements.
132///
133/// At this point, the sub-matrix (up to `RECURSIVE_LIMIT`×`RECURSIVE_LIMIT` elements)
134/// fits in L2 cache, so we switch to tiled transpose.
135#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
136const RECURSIVE_LIMIT: usize = 128;
137
138/// Minimum number of elements before enabling parallel processing.
139///
140/// Parallel transpose only pays off for large matrices because:
141/// - Thread spawn/join overhead (~1-10μs)
142/// - Cache coherency traffic between cores
143/// - Memory bandwidth becomes the bottleneck, not compute
144///
145/// At `PARALLEL_THRESHOLD` elements, the work per thread is large enough that
146/// parallelism overhead is amortized.
147#[cfg(all(target_arch = "aarch64", feature = "parallel"))]
148const PARALLEL_THRESHOLD: usize = 4 * 1024 * 1024;
149
150/// Transpose a matrix from row-major `input` to row-major `output`.
151///
152/// Given an input matrix with `height` rows and `width` columns, produces
153/// an output matrix with `width` rows and `height` columns.
154///
155/// # Memory Layout
156///
157/// Both input and output are stored in **row-major order**.
158///
159/// ```text
160/// Input (height=2, width=3): Output (height=3, width=2):
161///
162/// Row 0: [ a, b, c ] Row 0: [ a, d ]
163/// Row 1: [ d, e, f ] Row 1: [ b, e ]
164/// Row 2: [ c, f ]
165///
166/// Memory: [a, b, c, d, e, f] Memory: [a, d, b, e, c, f]
167/// ```
168///
169/// # Index Transformation
170///
171/// - Initial element at `input[row * width + col]`,
172/// - Transposed position is `output[col * height + row]`.
173///
174/// # Arguments
175///
176/// * `input` - Source matrix in row-major order
177/// * `output` - Destination buffer in row-major order
178/// * `width` - Number of columns in the input matrix
179/// * `height` - Number of rows in the input matrix
180///
181/// # Panics
182///
183/// Panics if:
184/// - `input.len() != width * height`
185/// - `output.len() != width * height`
186#[inline]
187pub fn transpose<T: Copy + Send + Sync>(
188 input: &[T],
189 output: &mut [T],
190 width: usize,
191 height: usize,
192) {
193 // Input validation
194 assert_eq!(
195 input.len(),
196 width * height,
197 "Input length {} doesn't match width*height = {}",
198 input.len(),
199 width * height
200 );
201 assert_eq!(
202 output.len(),
203 width * height,
204 "Output length {} doesn't match width*height = {}",
205 output.len(),
206 width * height
207 );
208
209 // Handle empty matrices
210 if width == 0 || height == 0 {
211 return;
212 }
213
214 // Architecture dispatch
215 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
216 {
217 // Use NEON-optimized path for 4-byte elements.
218 //
219 // This covers common field types like MontyField31.
220 //
221 // The alignment check matters as much as the size check: a type like
222 // `Complex<Mersenne31>` is 8 bytes but only 4-byte aligned, so it must
223 // not take the 8-byte path below, which assumes u64 alignment.
224 if core::mem::size_of::<T>() == 4 && core::mem::align_of::<T>() == 4 {
225 // SAFETY:
226 // - input/output lengths verified above
227 // - T is 4 bytes and 4-byte aligned, matching u32 size and alignment
228 // - Pointers derived from valid slices
229 unsafe {
230 transpose_neon_4b(
231 input.as_ptr().cast::<u32>(),
232 output.as_mut_ptr().cast::<u32>(),
233 width,
234 height,
235 );
236 }
237 return;
238 }
239
240 // Use NEON-optimized path for 8-byte elements.
241 //
242 // This covers 64-bit field types like Goldilocks.
243 // A 128-bit NEON register holds 2 u64 elements, so we use
244 // pairs of registers per row and a 1-stage butterfly.
245 if core::mem::size_of::<T>() == 8 && core::mem::align_of::<T>() == 8 {
246 // SAFETY:
247 // - input/output lengths verified above
248 // - T is 8 bytes and 8-byte aligned, matching u64 size and alignment
249 // - Pointers derived from valid slices
250 unsafe {
251 transpose_neon_8b(
252 input.as_ptr().cast::<u64>(),
253 output.as_mut_ptr().cast::<u64>(),
254 width,
255 height,
256 );
257 }
258 return;
259 }
260 }
261
262 // Fallback for non-ARM64 or unsupported element sizes.
263 super::portable::transpose(input, output, width, height);
264}
265
266/// Top-level NEON transpose dispatcher for 4-byte elements.
267///
268/// Selects the appropriate strategy based on matrix size:
269///
270/// ```text
271/// ┌───────────────────────────────────────────────────────────────────────────────────┐
272/// │ transpose_neon_4b │
273/// │ │ │
274/// │ ┌────────────────────────────────┼───────────────────────────────┐ │
275/// │ ▼ ▼ ▼ │
276/// │ len < SMALL_LEN SMALL_LEN ≤ len < MEDIUM_LEN len ≥ MEDIUM_LEN │
277/// │ │ │ │ │
278/// │ ▼ ▼ ▼ │
279/// │ scalar tiled TILE_SIZE×TILE_SIZE recursive │
280/// │ │ (→ tiled at │
281/// │ │ leaves) │
282/// │ │ │ │
283/// │ └───────────────┬───────────────┘ │
284/// │ ▼ │
285/// │ parallel (if ≥ PARALLEL_THRESHOLD │
286/// │ and feature enabled) │
287/// └───────────────────────────────────────────────────────────────────────────────────┘
288/// ```
289///
290/// # Safety
291///
292/// Caller must ensure `input` and `output` point to valid memory regions
293/// of at least `width * height` elements each.
294#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
295#[inline]
296unsafe fn transpose_neon_4b(input: *const u32, output: *mut u32, width: usize, height: usize) {
297 // Total number of elements in the matrix.
298 let len = width * height;
299
300 #[cfg(feature = "parallel")]
301 {
302 // Parallel path (if enabled and matrix is large enough)
303 if len >= PARALLEL_THRESHOLD {
304 // SAFETY: Caller guarantees valid pointers.
305 unsafe {
306 transpose_neon_4b_parallel(input, output, width, height);
307 }
308 return;
309 }
310 }
311
312 // Sequential path - choose strategy based on size
313 if len <= SMALL_LEN {
314 // Small matrix: simple scalar transpose.
315 //
316 // SAFETY: Caller guarantees valid pointers.
317 unsafe {
318 transpose_small_4b(input, output, width, height);
319 }
320 } else if len <= MEDIUM_LEN {
321 // Medium matrix: single-level `TILE_SIZE`×`TILE_SIZE` tiling.
322 //
323 // SAFETY: Caller guarantees valid pointers.
324 unsafe {
325 transpose_tiled_4b(input, output, width, height);
326 }
327 } else {
328 // Large matrix: recursive subdivision then tiling.
329 //
330 // This is the cache-oblivious approach.
331 // SAFETY: Caller guarantees valid pointers.
332 unsafe {
333 transpose_recursive_4b(input, output, 0, height, 0, width, width, height);
334 }
335 }
336}
337
338/// Parallel transpose for very large matrices (at least `PARALLEL_THRESHOLD` elements).
339///
340/// The work is split into stripes, one per thread, along the longer input dimension.
341///
342/// # Stripe Division
343///
344/// The output holds the transpose in row-major order.
345/// Input element `(r, c)` lands at output index `c * height + r`.
346///
347/// A wide input (more columns than rows) is split by columns.
348/// - Each thread owns a column band over every row.
349/// - Its writes form one contiguous block of output rows.
350///
351/// A tall or square input is split by rows.
352/// - Each thread owns a row band over every column.
353/// - Its input reads stay contiguous, one full row at a time.
354///
355/// ```text
356/// wide input: split by columns tall input: split by rows
357/// ┌──────┬──────┬──────┐ ┌────────────────────┐
358/// │ t0 │ t1 │ t2 │ │ t0 │
359/// │ cols │ cols │ cols │ ├────────────────────┤
360/// └──────┴──────┴──────┘ │ t1 │
361/// ├────────────────────┤
362/// │ t2 │
363/// └────────────────────┘
364/// ```
365///
366/// # Why the longer dimension
367///
368/// Splitting a wide input by rows would scatter each thread's writes:
369/// - Each thread gets a thin column band, written down the tall output
370/// with stride `height`.
371/// - Scattered stores stall on read-for-ownership and TLB traffic.
372///
373/// Splitting by columns keeps each thread's writes in one contiguous block.
374///
375/// # Data Race Safety
376///
377/// Each thread writes a disjoint output region, so no synchronization is needed.
378/// - A column band maps to a contiguous run of output rows, unique per thread.
379/// - A row band maps to a unique set of output columns.
380///
381/// # Safety
382///
383/// Caller must ensure valid pointers for `width * height` elements.
384#[cfg(all(target_arch = "aarch64", feature = "parallel"))]
385#[inline]
386unsafe fn transpose_neon_4b_parallel(
387 input: *const u32,
388 output: *mut u32,
389 width: usize,
390 height: usize,
391) {
392 use rayon::prelude::*;
393
394 // Number of available threads in the rayon thread pool.
395 let num_threads = rayon::current_num_threads();
396
397 // We use `AtomicUsize` to pass pointer addresses to threads.
398 //
399 // This is safe because:
400 // 1. We only read the addresses (Relaxed ordering is fine)
401 // 2. Each thread writes to a disjoint output region
402 let inp = AtomicUsize::new(input as usize);
403 let out = AtomicUsize::new(output as usize);
404
405 // Split the longer input dimension so each thread's output stays contiguous.
406 //
407 // A wide input is split by columns, a tall or square input by rows.
408 let split_cols = width > height;
409
410 // Length of the dimension being split.
411 let stripe_len = if split_cols { width } else { height };
412
413 // Share handed to each thread.
414 //
415 // The ceiling keeps the final thread from getting an oversized chunk.
416 let stripe_per_thread = stripe_len.div_ceil(num_threads);
417
418 (0..num_threads).into_par_iter().for_each(|thread_idx| {
419 // Half-open stripe `[start, end)` of the split dimension owned here.
420 let start = thread_idx * stripe_per_thread;
421 let end = (start + stripe_per_thread).min(stripe_len);
422
423 // Empty when there are more threads than stripe units.
424 if start < end {
425 // Recover the pointers from their atomic addresses.
426 let input_ptr = inp.load(Ordering::Relaxed) as *const u32;
427 let output_ptr = out.load(Ordering::Relaxed) as *mut u32;
428
429 // Map the stripe to an input region.
430 //
431 // A column stripe spans every row.
432 // A row stripe spans every column.
433 let (row_start, row_end, col_start, col_end) = if split_cols {
434 (0, height, start, end)
435 } else {
436 (start, end, 0, width)
437 };
438
439 // SAFETY:
440 // - Pointers are valid for `width * height` elements (from caller).
441 // - Stripes partition one dimension, so the per-thread output
442 // regions are disjoint and never aliased.
443 unsafe {
444 transpose_region_tiled_4b(
445 input_ptr, output_ptr, row_start, row_end, col_start, col_end, width, height,
446 );
447 }
448 }
449 });
450}
451
452/// Simple element-by-element transpose for small matrices.
453///
454/// For matrices with <= `SMALL_LEN` elements, the overhead of tiling isn't justified.
455/// Direct copying with good cache behavior is faster.
456///
457/// # Algorithm
458///
459/// For each position `(x, y)`:
460/// - Read from `input[y * width + x]`
461/// - Write to `output[x * height + y]`
462///
463/// # Loop Order
464///
465/// We iterate `x` in the outer loop to improve **output locality**.
466///
467/// This means consecutive writes go to consecutive memory addresses,
468/// which is better for the write-combining buffers.
469///
470/// # Safety
471///
472/// Caller must ensure valid pointers for `width * height` elements.
473#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
474#[inline]
475unsafe fn transpose_small_4b(input: *const u32, output: *mut u32, width: usize, height: usize) {
476 // Outer loop over columns (output rows).
477 for x in 0..width {
478 // Inner loop over rows (output columns).
479 for y in 0..height {
480 // Input index: row-major position of element (y, x).
481 let input_index = x + y * width;
482
483 // Output index: row-major position of element (x, y).
484 let output_index = y + x * height;
485
486 // SAFETY: Indices are within bounds by loop construction.
487 unsafe {
488 *output.add(output_index) = *input.add(input_index);
489 }
490 }
491 }
492}
493
494/// Tiled transpose using `TILE_SIZE`×`TILE_SIZE` tiles composed of `BLOCK_SIZE`×`BLOCK_SIZE` NEON blocks.
495///
496/// This is important for medium-sized matrices (`SMALL_LEN` to `MEDIUM_LEN` elements).
497///
498/// # Tiling Strategy
499///
500/// - The matrix is divided into `TILE_SIZE`×`TILE_SIZE` tiles.
501/// - Each tile is further divided into (`TILE_SIZE`/`BLOCK_SIZE`)^2 blocks that are transposed using NEON SIMD.
502///
503/// ```text
504/// Matrix (e.g., 64×48):
505/// ┌─────────────────────────────────────────────────────────────────────────────────────┐
506/// │ Tile(0,0) │ Tile(1,0) │ Tile(2,0) │ Tile(3,0) │rem_x │
507/// │ TILE_SIZE×TILE_SIZE│ TILE_SIZE×TILE_SIZE│ TILE_SIZE×TILE_SIZE│ TILE_SIZE×.. │ │
508/// ├────────────────────┼────────────────────┼────────────────────┼───────────────┼──────┤
509/// │ Tile(0,1) │ Tile(1,1) │ Tile(2,1) │ Tile(3,1) │rem_x │
510/// │ TILE_SIZE×TILE_SIZE│ TILE_SIZE×TILE_SIZE│ TILE_SIZE×TILE_SIZE│ TILE_SIZE×.. │ │
511/// ├────────────────────┼────────────────────┼────────────────────┼───────────────┼──────┤
512/// │ Tile(0,2) │ Tile(1,2) │ Tile(2,2) │ Tile(3,2) │rem_x │
513/// │ TILE_SIZE×TILE_SIZE│ TILE_SIZE×TILE_SIZE│ TILE_SIZE×TILE_SIZE│ TILE_SIZE×.. │ │
514/// └────────────────────┴────────────────────┴────────────────────┴───────────────┴──────┘
515/// rem_y
516/// ```
517///
518/// Remainders (`rem_x`, `rem_y`) are handled with scalar transpose.
519///
520/// # Safety
521///
522/// Caller must ensure valid pointers for `width * height` elements.
523#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
524#[inline]
525unsafe fn transpose_tiled_4b(input: *const u32, output: *mut u32, width: usize, height: usize) {
526 // Compute tile counts and remainders
527
528 // Number of complete `TILE_SIZE`×`TILE_SIZE` tiles in each dimension.
529 let x_tile_count = width / TILE_SIZE;
530 let y_tile_count = height / TILE_SIZE;
531
532 // Leftover elements that don't fit in complete tiles.
533 let remainder_x = width - x_tile_count * TILE_SIZE;
534 let remainder_y = height - y_tile_count * TILE_SIZE;
535
536 // Process complete `TILE_SIZE`×`TILE_SIZE` tiles
537
538 // Iterate over tile rows.
539 for y_tile in 0..y_tile_count {
540 // Iterate over tile columns.
541 for x_tile in 0..x_tile_count {
542 // Top-left corner of this tile.
543 let x_start = x_tile * TILE_SIZE;
544 let y_start = y_tile * TILE_SIZE;
545
546 // Transpose this `TILE_SIZE`×`TILE_SIZE` tile
547 //
548 // SAFETY: Tile coordinates are within bounds.
549 unsafe {
550 transpose_tile_16x16_neon(input, output, width, height, x_start, y_start);
551 }
552 }
553
554 // Handle partial column tiles (right edge)
555
556 // Elements in columns [x_tile_count * TILE_SIZE, width) don't form a complete tile.
557 // Use scalar transpose for these.
558 if remainder_x > 0 {
559 // SAFETY: Coordinates are within bounds.
560 unsafe {
561 transpose_block_scalar(
562 input,
563 output,
564 width,
565 height,
566 x_tile_count * TILE_SIZE, // x_start
567 y_tile * TILE_SIZE, // y_start
568 remainder_x, // block_width
569 TILE_SIZE, // block_height
570 );
571 }
572 }
573 }
574
575 // Handle partial row tiles (bottom edge)
576
577 // Elements in rows [y_tile_count * TILE_SIZE, height) don't form complete tiles.
578 if remainder_y > 0 {
579 // Process bottom edge tiles (except corner).
580 for x_tile in 0..x_tile_count {
581 // SAFETY: Coordinates are within bounds.
582 unsafe {
583 transpose_block_scalar(
584 input,
585 output,
586 width,
587 height,
588 x_tile * TILE_SIZE, // x_start
589 y_tile_count * TILE_SIZE, // y_start
590 TILE_SIZE, // block_width
591 remainder_y, // block_height
592 );
593 }
594 }
595
596 // Handle corner block (bottom-right)
597
598 // The corner block is the intersection of right and bottom remainders.
599 if remainder_x > 0 {
600 // SAFETY: Coordinates are within bounds.
601 unsafe {
602 transpose_block_scalar(
603 input,
604 output,
605 width,
606 height,
607 x_tile_count * TILE_SIZE, // x_start
608 y_tile_count * TILE_SIZE, // y_start
609 remainder_x, // block_width
610 remainder_y, // block_height
611 );
612 }
613 }
614 }
615}
616
617/// Recursive cache-oblivious transpose for large matrices.
618///
619/// This algorithm recursively subdivides the matrix until sub-blocks fit
620/// in cache, then uses tiled transpose on the leaves.
621///
622/// # Cache-Oblivious Design
623///
624/// The key insight is that we don't need to know cache sizes explicitly.
625///
626/// By recursively halving the problem, we eventually reach a size that
627/// fits in any level of cache (L1, L2, or L3).
628///
629/// # Recursion Pattern
630///
631/// At each level, we split along the **longer dimension**:
632///
633/// ```text
634/// Wide matrix (cols > rows): Tall matrix (rows ≥ cols):
635/// Split vertically Split horizontally
636///
637/// ┌─────────┬─────────┐ ┌───────────────────┐
638/// │ │ │ │ │
639/// │ Left │ Right │ │ Top │
640/// │ │ │ │ │
641/// │ │ │ ├───────────────────┤
642/// │ │ │ │ │
643/// └─────────┴─────────┘ │ Bottom │
644/// │ │
645/// └───────────────────┘
646/// ```
647///
648/// # Base Case
649///
650/// We stop recursing when both dimensions are ≤ `RECURSIVE_LIMIT` elements (or ≤ 2,
651/// which is a degenerate case). At this point, the sub-matrix fits in
652/// L2 cache (~64KB for `RECURSIVE_LIMIT`×`RECURSIVE_LIMIT`×4 bytes), so we use tiled transpose.
653///
654/// # Parameters
655///
656/// The function uses coordinate ranges rather than creating sub-arrays:
657/// - `row_start..row_end`: Row range in the original matrix
658/// - `col_start..col_end`: Column range in the original matrix
659/// - `total_cols`, `total_rows`: Original matrix dimensions (for stride calculations)
660///
661/// # Safety
662///
663/// Caller must ensure valid pointers and that coordinate ranges are within bounds.
664#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
665#[allow(clippy::too_many_arguments)]
666unsafe fn transpose_recursive_4b(
667 input: *const u32,
668 output: *mut u32,
669 row_start: usize,
670 row_end: usize,
671 col_start: usize,
672 col_end: usize,
673 total_cols: usize,
674 total_rows: usize,
675) {
676 // Compute sub-matrix dimensions
677 let nbr_rows = row_end - row_start;
678 let nbr_cols = col_end - col_start;
679
680 // Base case: small enough to use tiled transpose
681
682 // Stop recursing when:
683 // 1. Both dimensions ≤ RECURSIVE_LIMIT (fits in cache), OR
684 // 2. Either dimension ≤ 2 (degenerate case, no benefit from recursion)
685 if (nbr_rows <= RECURSIVE_LIMIT && nbr_cols <= RECURSIVE_LIMIT)
686 || nbr_rows <= 2
687 || nbr_cols <= 2
688 {
689 // SAFETY: Caller ensures valid pointers and bounds.
690 unsafe {
691 transpose_region_tiled_4b(
692 input, output, row_start, row_end, col_start, col_end, total_cols, total_rows,
693 );
694 }
695 return;
696 }
697
698 // Recursive case: split along the longer dimension
699 if nbr_rows >= nbr_cols {
700 // Split horizontally (by rows)
701
702 // Midpoint of the row range.
703 let mid = row_start + (nbr_rows / 2);
704
705 // Recurse on top half.
706 // SAFETY: mid is within [row_start, row_end].
707 unsafe {
708 transpose_recursive_4b(
709 input, output, row_start, mid, col_start, col_end, total_cols, total_rows,
710 );
711 }
712
713 // Recurse on bottom half.
714 // SAFETY: mid is within [row_start, row_end].
715 unsafe {
716 transpose_recursive_4b(
717 input, output, mid, row_end, col_start, col_end, total_cols, total_rows,
718 );
719 }
720 } else {
721 // Split vertically (by columns)
722
723 // Midpoint of the column range.
724 let mid = col_start + (nbr_cols / 2);
725
726 // Recurse on left half.
727 // SAFETY: mid is within [col_start, col_end].
728 unsafe {
729 transpose_recursive_4b(
730 input, output, row_start, row_end, col_start, mid, total_cols, total_rows,
731 );
732 }
733
734 // Recurse on right half.
735 // SAFETY: mid is within [col_start, col_end].
736 unsafe {
737 transpose_recursive_4b(
738 input, output, row_start, row_end, mid, col_end, total_cols, total_rows,
739 );
740 }
741 }
742}
743
744/// Tiled transpose for a rectangular region within a larger matrix.
745///
746/// It operates on a sub-region defined by coordinate ranges.
747///
748/// Used as the base case of recursive transpose and for parallel stripe processing.
749///
750/// # Coordinate System
751///
752/// ```text
753/// Original matrix (total_cols × total_rows):
754/// ┌─────────────────────────────────────────────────────────┐
755/// │ │
756/// │ (col_start, row_start) │
757/// │ ┌─────────────────────┐ │
758/// │ │ │ │
759/// │ │ Region to │ │
760/// │ │ transpose │ │
761/// │ │ │ │
762/// │ └─────────────────────┘ │
763/// │ (col_end, row_end) │
764/// │ │
765/// └─────────────────────────────────────────────────────────┘
766/// ```
767///
768/// # Safety
769///
770/// Caller must ensure:
771/// - Valid pointers for `total_cols * total_rows` elements
772/// - `row_start < row_end <= total_rows`
773/// - `col_start < col_end <= total_cols`
774#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
775#[inline]
776#[allow(clippy::too_many_arguments)]
777unsafe fn transpose_region_tiled_4b(
778 input: *const u32,
779 output: *mut u32,
780 row_start: usize,
781 row_end: usize,
782 col_start: usize,
783 col_end: usize,
784 total_cols: usize,
785 total_rows: usize,
786) {
787 // Compute region dimensions and tile counts
788
789 // Dimensions of the region to transpose.
790 let nbr_cols = col_end - col_start;
791 let nbr_rows = row_end - row_start;
792
793 // Number of complete `TILE_SIZE`×`TILE_SIZE` tiles in each dimension.
794 let x_tile_count = nbr_cols / TILE_SIZE;
795 let y_tile_count = nbr_rows / TILE_SIZE;
796
797 // Leftover elements that don't fit in complete tiles.
798 let remainder_x = nbr_cols - x_tile_count * TILE_SIZE;
799 let remainder_y = nbr_rows - y_tile_count * TILE_SIZE;
800
801 // Process complete `TILE_SIZE`×`TILE_SIZE` tiles
802 for y_tile in 0..y_tile_count {
803 for x_tile in 0..x_tile_count {
804 // Coordinates of this tile's top-left corner in the original matrix.
805 let col = col_start + x_tile * TILE_SIZE;
806 let row = row_start + y_tile * TILE_SIZE;
807
808 // SAFETY: Tile coordinates are within the region bounds.
809 // Uses the buffered tile function: for large matrices the output
810 // is likely in L3/RAM, so L1 buffering + write prefetching avoids
811 // RFO stalls on scattered output writes.
812 unsafe {
813 transpose_tile_16x16_neon_buffered(input, output, total_cols, total_rows, col, row);
814 }
815 }
816
817 // Handle partial column tiles (right edge of region)
818 if remainder_x > 0 {
819 // SAFETY: Coordinates are within region bounds.
820 unsafe {
821 transpose_block_scalar(
822 input,
823 output,
824 total_cols,
825 total_rows,
826 col_start + x_tile_count * TILE_SIZE, // x_start
827 row_start + y_tile * TILE_SIZE, // y_start
828 remainder_x, // block_width
829 TILE_SIZE, // block_height
830 );
831 }
832 }
833 }
834
835 // Handle partial row tiles (bottom edge of region)
836 if remainder_y > 0 {
837 for x_tile in 0..x_tile_count {
838 // SAFETY: Coordinates are within region bounds.
839 unsafe {
840 transpose_block_scalar(
841 input,
842 output,
843 total_cols,
844 total_rows,
845 col_start + x_tile * TILE_SIZE, // x_start
846 row_start + y_tile_count * TILE_SIZE, // y_start
847 TILE_SIZE, // block_width
848 remainder_y, // block_height
849 );
850 }
851 }
852
853 // Handle corner block (bottom-right of region)
854 if remainder_x > 0 {
855 // SAFETY: Coordinates are within region bounds.
856 unsafe {
857 transpose_block_scalar(
858 input,
859 output,
860 total_cols,
861 total_rows,
862 col_start + x_tile_count * TILE_SIZE, // x_start
863 row_start + y_tile_count * TILE_SIZE, // y_start
864 remainder_x, // block_width
865 remainder_y, // block_height
866 );
867 }
868 }
869 }
870}
871
872/// Transpose a complete 16×16 tile using NEON SIMD (direct-to-output).
873///
874/// A 16×16 tile is processed as a 4×4 grid of 4×4 NEON blocks.
875/// This function is fully unrolled for maximum performance.
876///
877/// Used by the **medium tiled path** where the output likely fits in L2 cache
878/// and the overhead of L1 buffering isn't justified.
879///
880/// # Safety
881///
882/// Caller must ensure:
883/// - Valid pointers for the full matrix
884#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
885/// - `y_start + 16 <= height`
886#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
887#[inline]
888unsafe fn transpose_tile_16x16_neon(
889 input: *const u32,
890 output: *mut u32,
891 width: usize,
892 height: usize,
893 x_start: usize,
894 y_start: usize,
895) {
896 unsafe {
897 // Block Row 0 (input rows y_start..y_start+4)
898 let inp = input.add(y_start * width + x_start);
899 let out = output.add(x_start * height + y_start);
900 transpose_4x4_neon(inp, out, width, height);
901 transpose_4x4_neon(inp.add(4), out.add(4 * height), width, height);
902 transpose_4x4_neon(inp.add(8), out.add(8 * height), width, height);
903 transpose_4x4_neon(inp.add(12), out.add(12 * height), width, height);
904
905 // Block Row 1 (input rows y_start+4..y_start+8)
906 let inp = input.add((y_start + 4) * width + x_start);
907 let out = output.add(x_start * height + y_start + 4);
908 transpose_4x4_neon(inp, out, width, height);
909 transpose_4x4_neon(inp.add(4), out.add(4 * height), width, height);
910 transpose_4x4_neon(inp.add(8), out.add(8 * height), width, height);
911 transpose_4x4_neon(inp.add(12), out.add(12 * height), width, height);
912
913 // Block Row 2 (input rows y_start+8..y_start+12)
914 let inp = input.add((y_start + 8) * width + x_start);
915 let out = output.add(x_start * height + y_start + 8);
916 transpose_4x4_neon(inp, out, width, height);
917 transpose_4x4_neon(inp.add(4), out.add(4 * height), width, height);
918 transpose_4x4_neon(inp.add(8), out.add(8 * height), width, height);
919 transpose_4x4_neon(inp.add(12), out.add(12 * height), width, height);
920
921 // Block Row 3 (input rows y_start+12..y_start+16)
922 let inp = input.add((y_start + 12) * width + x_start);
923 let out = output.add(x_start * height + y_start + 12);
924 transpose_4x4_neon(inp, out, width, height);
925 transpose_4x4_neon(inp.add(4), out.add(4 * height), width, height);
926 transpose_4x4_neon(inp.add(8), out.add(8 * height), width, height);
927 transpose_4x4_neon(inp.add(12), out.add(12 * height), width, height);
928 }
929}
930
931/// Transpose a complete 16×16 tile using NEON SIMD with L1 buffering.
932///
933/// Same grid of 4×4 NEON blocks, but transposed into a stack-allocated buffer
934/// first, then flushed to the output with write prefetching (`PRFM PSTL1KEEP`).
935///
936/// Used by the **recursive/parallel path** for large matrices (≥ `MEDIUM_LEN`)
937/// where the output is in L3/RAM and direct scattered writes would stall on
938/// Read-For-Ownership (RFO) cache line fetches.
939///
940/// # Safety
941///
942/// Caller must ensure:
943/// - Valid pointers for the full matrix
944/// - `x_start + 16 <= width`
945/// - `y_start + 16 <= height`
946#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
947#[inline]
948unsafe fn transpose_tile_16x16_neon_buffered(
949 input: *const u32,
950 output: *mut u32,
951 width: usize,
952 height: usize,
953 x_start: usize,
954 y_start: usize,
955) {
956 // Stack buffer for L1-hot transpose (1 KB for u32).
957 // MaybeUninit avoids unnecessary zero-initialization; every element
958 // is written by the NEON blocks before the copy reads it.
959 let mut buffer = MaybeUninit::<[u32; TILE_SIZE * TILE_SIZE]>::uninit();
960 let buf = buffer.as_mut_ptr().cast::<u32>();
961
962 unsafe {
963 // Transpose 4×4 grid of NEON blocks into the buffer.
964 // Buffer layout: buf[col * TILE_SIZE + row] = transposed element.
965 // Buffer write stride is TILE_SIZE (contiguous in L1) vs. `height` (scattered).
966
967 // Block Row 0 (input rows y_start..y_start+4)
968 let inp = input.add(y_start * width + x_start);
969 transpose_4x4_neon(inp, buf, width, TILE_SIZE);
970 transpose_4x4_neon(inp.add(4), buf.add(4 * TILE_SIZE), width, TILE_SIZE);
971 transpose_4x4_neon(inp.add(8), buf.add(8 * TILE_SIZE), width, TILE_SIZE);
972 transpose_4x4_neon(inp.add(12), buf.add(12 * TILE_SIZE), width, TILE_SIZE);
973
974 // Block Row 1 (input rows y_start+4..y_start+8)
975 let inp = input.add((y_start + 4) * width + x_start);
976 transpose_4x4_neon(inp, buf.add(4), width, TILE_SIZE);
977 transpose_4x4_neon(inp.add(4), buf.add(4 * TILE_SIZE + 4), width, TILE_SIZE);
978 transpose_4x4_neon(inp.add(8), buf.add(8 * TILE_SIZE + 4), width, TILE_SIZE);
979 transpose_4x4_neon(inp.add(12), buf.add(12 * TILE_SIZE + 4), width, TILE_SIZE);
980
981 // Block Row 2 (input rows y_start+8..y_start+12)
982 let inp = input.add((y_start + 8) * width + x_start);
983 transpose_4x4_neon(inp, buf.add(8), width, TILE_SIZE);
984 transpose_4x4_neon(inp.add(4), buf.add(4 * TILE_SIZE + 8), width, TILE_SIZE);
985 transpose_4x4_neon(inp.add(8), buf.add(8 * TILE_SIZE + 8), width, TILE_SIZE);
986 transpose_4x4_neon(inp.add(12), buf.add(12 * TILE_SIZE + 8), width, TILE_SIZE);
987
988 // Block Row 3 (input rows y_start+12..y_start+16)
989 let inp = input.add((y_start + 12) * width + x_start);
990 transpose_4x4_neon(inp, buf.add(12), width, TILE_SIZE);
991 transpose_4x4_neon(inp.add(4), buf.add(4 * TILE_SIZE + 12), width, TILE_SIZE);
992 transpose_4x4_neon(inp.add(8), buf.add(8 * TILE_SIZE + 12), width, TILE_SIZE);
993 transpose_4x4_neon(inp.add(12), buf.add(12 * TILE_SIZE + 12), width, TILE_SIZE);
994
995 // Flush buffer to output with write prefetching.
996 // Each iteration copies TILE_SIZE u32s (64 bytes = 1 cache line) from the
997 // L1-hot buffer to one output row. Prefetch brings the next output cache
998 // line into exclusive state, avoiding RFO stalls.
999 prefetch_write(output.add(x_start * height + y_start) as *const u8);
1000 for c in 0..TILE_SIZE {
1001 if c + 1 < TILE_SIZE {
1002 prefetch_write(output.add((x_start + c + 1) * height + y_start) as *const u8);
1003 }
1004 core::ptr::copy_nonoverlapping(
1005 buf.add(c * TILE_SIZE),
1006 output.add((x_start + c) * height + y_start),
1007 TILE_SIZE,
1008 );
1009 }
1010 }
1011}
1012
1013/// Scalar transpose for an arbitrary rectangular block.
1014///
1015/// Used for handling edge cases where dimensions don't align to tile boundaries.
1016/// Falls back to simple element-by-element copying.
1017///
1018/// # When Used
1019///
1020/// - Right edge: `block_width < TILE_SIZE`
1021/// - Bottom edge: `block_height < TILE_SIZE`
1022/// - Bottom-right corner: both dimensions < `TILE_SIZE`
1023///
1024/// # Safety
1025///
1026#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1027/// - Valid pointers for the full matrix
1028/// - `x_start + block_width <= width`
1029/// - `y_start + block_height <= height`
1030#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1031#[inline]
1032#[allow(clippy::too_many_arguments)]
1033unsafe fn transpose_block_scalar(
1034 input: *const u32,
1035 output: *mut u32,
1036 width: usize,
1037 height: usize,
1038 x_start: usize,
1039 y_start: usize,
1040 block_width: usize,
1041 block_height: usize,
1042) {
1043 // Iterate over block columns (becomes output rows).
1044 for inner_x in 0..block_width {
1045 // Iterate over block rows (becomes output columns).
1046 for inner_y in 0..block_height {
1047 // Absolute coordinates in the original matrix.
1048 let x = x_start + inner_x;
1049 let y = y_start + inner_y;
1050
1051 // Input index: row-major position of (y, x).
1052 let input_index = x + y * width;
1053
1054 // Output index: row-major position of (x, y) in transposed matrix.
1055 let output_index = y + x * height;
1056
1057 // SAFETY: Indices are within bounds by construction.
1058 unsafe {
1059 *output.add(output_index) = *input.add(input_index);
1060 }
1061 }
1062 }
1063}
1064
1065/// Transpose a 4×4 block of 32-bit elements using NEON SIMD.
1066///
1067/// This is the fundamental building block of the entire transpose algorithm.
1068///
1069/// It transposes a 4×4 block entirely within NEON registers
1070/// using a two-stage butterfly network.
1071///
1072/// # Memory Layout
1073///
1074/// Input (4 rows, stride = `src_stride`):
1075/// ```text
1076/// src + 0*stride: [ a00, a01, a02, a03 ] → q0
1077/// src + 1*stride: [ a10, a11, a12, a13 ] → q1
1078/// src + 2*stride: [ a20, a21, a22, a23 ] → q2
1079/// src + 3*stride: [ a30, a31, a32, a33 ] → q3
1080/// ```
1081///
1082/// Output (4 rows, stride = `dst_stride`):
1083/// ```text
1084/// dst + 0*stride: [ a00, a10, a20, a30 ] ← r0
1085/// dst + 1*stride: [ a01, a11, a21, a31 ] ← r1
1086/// dst + 2*stride: [ a02, a12, a22, a32 ] ← r2
1087/// dst + 3*stride: [ a03, a13, a23, a33 ] ← r3
1088/// ```
1089///
1090/// # Butterfly Network Algorithm
1091///
1092/// The transpose is performed in two stages using `TRN1`/`TRN2` instructions:
1093///
1094/// ## Stage 1: 32-bit Transpose
1095///
1096/// - `TRN1` takes **even-indexed** elements,
1097/// - `TRN2` takes **odd-indexed** elements.
1098///
1099/// ```text
1100/// TRN1(q0, q1) = [ a00, a10, a02, a12 ] (even indices: 0, 2)
1101/// TRN2(q0, q1) = [ a01, a11, a03, a13 ] (odd indices: 1, 3)
1102/// TRN1(q2, q3) = [ a20, a30, a22, a32 ]
1103/// TRN2(q2, q3) = [ a21, a31, a23, a33 ]
1104/// ```
1105///
1106/// ## Stage 2: 64-bit Transpose
1107///
1108/// Reinterpret as 64-bit elements and transpose again:
1109///
1110/// ```text
1111/// TRN1_64(t0, t2) = [ a00, a10 | a20, a30 ] → r0
1112/// TRN2_64(t0, t2) = [ a02, a12 | a22, a32 ] → r2
1113/// TRN1_64(t1, t3) = [ a01, a11 | a21, a31 ] → r1
1114/// TRN2_64(t1, t3) = [ a03, a13 | a23, a33 ] → r3
1115/// ```
1116///
1117/// # Explanations
1118///
1119/// The butterfly network swaps elements at progressively larger distances:
1120/// - Stage 1: Swaps elements 1 apart (within 64-bit pairs)
1121/// - Stage 2: Swaps elements 2 apart (between 64-bit halves)
1122///
1123/// This is analogous to the bit-reversal pattern in FFT algorithms.
1124///
1125/// # Performance
1126///
1127/// - **4 loads** (vld1q_u32): 4 cycles
1128/// - **8 permutes** (vtrn): ~8 cycles (pipelined)
1129/// - **4 stores** (vst1q_u32): 4 cycles
1130/// - **Total**: ~16 cycles for 16 elements = **1 cycle/element**
1131///
1132/// # Safety
1133#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1134/// Caller must ensure:
1135/// - `src` is valid for reading 4 rows of `src_stride` elements each
1136/// - `dst` is valid for writing 4 rows of `dst_stride` elements each
1137/// - The first 4 elements of each row are accessible
1138#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1139#[inline(always)]
1140unsafe fn transpose_4x4_neon(src: *const u32, dst: *mut u32, src_stride: usize, dst_stride: usize) {
1141 unsafe {
1142 // Phase 1: Load 4 rows into NEON registers
1143 //
1144 // Each vld1q_u32 loads 4 consecutive u32s (16 bytes = 128 bits).
1145 // Total: 64 bytes = one cache line on most ARM64 CPUs.
1146
1147 // Row 0: [a00, a01, a02, a03]
1148 let q0 = vld1q_u32(src);
1149 // Row 1: [a10, a11, a12, a13]
1150 let q1 = vld1q_u32(src.add(src_stride));
1151 // Row 2: [a20, a21, a22, a23]
1152 let q2 = vld1q_u32(src.add(2 * src_stride));
1153 // Row 3: [a30, a31, a32, a33]
1154 let q3 = vld1q_u32(src.add(3 * src_stride));
1155
1156 // Phase 2: Stage 1 - Transpose 2×2 blocks of 32-bit elements
1157 //
1158 // vtrn1q_u32(a, b): Takes elements at even indices from a and b
1159 // - Result: [a[0], b[0], a[2], b[2]]
1160 //
1161 // vtrn2q_u32(a, b): Takes elements at odd indices from a and b
1162 // - Result: [a[1], b[1], a[3], b[3]]
1163
1164 let t0_0 = vtrn1q_u32(q0, q1); // [a00, a10, a02, a12]
1165 let t0_1 = vtrn2q_u32(q0, q1); // [a01, a11, a03, a13]
1166 let t0_2 = vtrn1q_u32(q2, q3); // [a20, a30, a22, a32]
1167 let t0_3 = vtrn2q_u32(q2, q3); // [a21, a31, a23, a33]
1168
1169 // Phase 3: Stage 2 - Transpose 2×2 blocks of 64-bit elements
1170 //
1171 // Reinterpret u32x4 as u64x2, then transpose.
1172 // This swaps the 64-bit halves of the vectors.
1173 //
1174 // vtrn1q_u64(a, b): [a.lo, b.lo]
1175 // vtrn2q_u64(a, b): [a.hi, b.hi]
1176
1177 // r0 = [a00, a10, a20, a30] (column 0 of input → row 0 of output)
1178 let r0 = vreinterpretq_u32_u64(vtrn1q_u64(
1179 vreinterpretq_u64_u32(t0_0),
1180 vreinterpretq_u64_u32(t0_2),
1181 ));
1182
1183 // r2 = [a02, a12, a22, a32] (column 2 of input → row 2 of output)
1184 let r2 = vreinterpretq_u32_u64(vtrn2q_u64(
1185 vreinterpretq_u64_u32(t0_0),
1186 vreinterpretq_u64_u32(t0_2),
1187 ));
1188
1189 // r1 = [a01, a11, a21, a31] (column 1 of input → row 1 of output)
1190 let r1 = vreinterpretq_u32_u64(vtrn1q_u64(
1191 vreinterpretq_u64_u32(t0_1),
1192 vreinterpretq_u64_u32(t0_3),
1193 ));
1194
1195 // r3 = [a03, a13, a23, a33] (column 3 of input → row 3 of output)
1196 let r3 = vreinterpretq_u32_u64(vtrn2q_u64(
1197 vreinterpretq_u64_u32(t0_1),
1198 vreinterpretq_u64_u32(t0_3),
1199 ));
1200
1201 // Phase 4: Store 4 transposed rows
1202 //
1203 // Store row 0 of output
1204 vst1q_u32(dst, r0);
1205 // Store row 1 of output
1206 vst1q_u32(dst.add(dst_stride), r1);
1207 // Store row 2 of output
1208 vst1q_u32(dst.add(2 * dst_stride), r2);
1209 // Store row 3 of output
1210 vst1q_u32(dst.add(3 * dst_stride), r3);
1211 }
1212}
1213
1214// ============================================================================
1215// 8-byte (u64) NEON transpose functions
1216//
1217// These are analogous to the 4-byte functions above, but operate on u64
1218// elements. Since a 128-bit NEON register holds 2 u64 elements, each row
1219// of a 4×4 block requires 2 registers (8 total for a block). The transpose
1220// uses a single-stage butterfly with vtrn1q_u64/vtrn2q_u64 on four 2×2
1221// sub-blocks.
1222// ============================================================================
1223
1224/// Top-level NEON transpose dispatcher for 8-byte elements.
1225///
1226/// Selects the appropriate strategy based on matrix size, mirroring
1227/// `transpose_neon_4b` but for u64 elements.
1228///
1229/// # Safety
1230///
1231/// Caller must ensure `input` and `output` point to valid memory regions
1232/// of at least `width * height` elements each.
1233#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1234#[inline]
1235unsafe fn transpose_neon_8b(input: *const u64, output: *mut u64, width: usize, height: usize) {
1236 let len = width * height;
1237
1238 #[cfg(feature = "parallel")]
1239 {
1240 if len >= PARALLEL_THRESHOLD {
1241 unsafe {
1242 transpose_neon_8b_parallel(input, output, width, height);
1243 }
1244 return;
1245 }
1246 }
1247
1248 if len <= SMALL_LEN {
1249 unsafe {
1250 transpose_small_8b(input, output, width, height);
1251 }
1252 } else if len <= MEDIUM_LEN {
1253 unsafe {
1254 transpose_tiled_8b(input, output, width, height);
1255 }
1256 } else {
1257 unsafe {
1258 transpose_recursive_8b(input, output, 0, height, 0, width, width, height);
1259 }
1260 }
1261}
1262
1263/// Parallel transpose for very large matrices of 8-byte elements.
1264///
1265/// The work is split into stripes, one per thread, along the longer input dimension.
1266///
1267/// # Stripe Division
1268///
1269/// The output holds the transpose in row-major order.
1270/// Input element `(r, c)` lands at output index `c * height + r`.
1271///
1272/// A wide input (more columns than rows) is split by columns.
1273/// - Each thread owns a column band over every row.
1274/// - Its writes form one contiguous block of output rows.
1275///
1276/// A tall or square input is split by rows.
1277/// - Each thread owns a row band over every column.
1278/// - Its input reads stay contiguous, one full row at a time.
1279///
1280/// ```text
1281/// wide input: split by columns tall input: split by rows
1282/// ┌──────┬──────┬──────┐ ┌────────────────────┐
1283/// │ t0 │ t1 │ t2 │ │ t0 │
1284/// │ cols │ cols │ cols │ ├────────────────────┤
1285/// └──────┴──────┴──────┘ │ t1 │
1286/// ├────────────────────┤
1287/// │ t2 │
1288/// └────────────────────┘
1289/// ```
1290///
1291/// # Why the longer dimension
1292///
1293/// Splitting a wide input by rows would scatter each thread's writes:
1294/// - Each thread gets a thin column band, written down the tall output
1295/// with stride `height`.
1296/// - Scattered stores stall on read-for-ownership and TLB traffic.
1297///
1298/// Splitting by columns keeps each thread's writes in one contiguous block.
1299///
1300/// # Data Race Safety
1301///
1302/// Each thread writes a disjoint output region, so no synchronization is needed.
1303/// - A column band maps to a contiguous run of output rows, unique per thread.
1304/// - A row band maps to a unique set of output columns.
1305///
1306/// # Safety
1307///
1308/// Caller must ensure valid pointers for `width * height` elements.
1309#[cfg(all(target_arch = "aarch64", feature = "parallel"))]
1310#[inline]
1311unsafe fn transpose_neon_8b_parallel(
1312 input: *const u64,
1313 output: *mut u64,
1314 width: usize,
1315 height: usize,
1316) {
1317 use rayon::prelude::*;
1318
1319 // Number of available threads in the rayon thread pool.
1320 let num_threads = rayon::current_num_threads();
1321
1322 // We use `AtomicUsize` to pass pointer addresses to threads.
1323 //
1324 // This is safe because:
1325 // 1. We only read the addresses (Relaxed ordering is fine)
1326 // 2. Each thread writes to a disjoint output region
1327 let inp = AtomicUsize::new(input as usize);
1328 let out = AtomicUsize::new(output as usize);
1329
1330 // Split the longer input dimension so each thread's output stays contiguous.
1331 //
1332 // A wide input is split by columns, a tall or square input by rows.
1333 let split_cols = width > height;
1334
1335 // Length of the dimension being split.
1336 let stripe_len = if split_cols { width } else { height };
1337
1338 // Share handed to each thread.
1339 //
1340 // The ceiling keeps the final thread from getting an oversized chunk.
1341 let stripe_per_thread = stripe_len.div_ceil(num_threads);
1342
1343 (0..num_threads).into_par_iter().for_each(|thread_idx| {
1344 // Half-open stripe `[start, end)` of the split dimension owned here.
1345 let start = thread_idx * stripe_per_thread;
1346 let end = (start + stripe_per_thread).min(stripe_len);
1347
1348 // Empty when there are more threads than stripe units.
1349 if start < end {
1350 // Recover the pointers from their atomic addresses.
1351 let input_ptr = inp.load(Ordering::Relaxed) as *const u64;
1352 let output_ptr = out.load(Ordering::Relaxed) as *mut u64;
1353
1354 // Map the stripe to an input region.
1355 //
1356 // A column stripe spans every row.
1357 // A row stripe spans every column.
1358 let (row_start, row_end, col_start, col_end) = if split_cols {
1359 (0, height, start, end)
1360 } else {
1361 (start, end, 0, width)
1362 };
1363
1364 // SAFETY:
1365 // - Pointers are valid for `width * height` elements (from caller).
1366 // - Stripes partition one dimension, so the per-thread output
1367 // regions are disjoint and never aliased.
1368 unsafe {
1369 transpose_region_tiled_8b(
1370 input_ptr, output_ptr, row_start, row_end, col_start, col_end, width, height,
1371 );
1372 }
1373 }
1374 });
1375}
1376
1377/// Simple element-by-element transpose for small matrices of 8-byte elements.
1378///
1379/// # Safety
1380///
1381/// Caller must ensure valid pointers for `width * height` elements.
1382#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1383#[inline]
1384unsafe fn transpose_small_8b(input: *const u64, output: *mut u64, width: usize, height: usize) {
1385 for x in 0..width {
1386 for y in 0..height {
1387 let input_index = x + y * width;
1388 let output_index = y + x * height;
1389
1390 unsafe {
1391 *output.add(output_index) = *input.add(input_index);
1392 }
1393 }
1394 }
1395}
1396
1397/// Tiled transpose using 16×16 tiles for 8-byte elements.
1398///
1399/// # Safety
1400///
1401/// Caller must ensure valid pointers for `width * height` elements.
1402#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1403#[inline]
1404unsafe fn transpose_tiled_8b(input: *const u64, output: *mut u64, width: usize, height: usize) {
1405 let x_tile_count = width / TILE_SIZE;
1406 let y_tile_count = height / TILE_SIZE;
1407
1408 let remainder_x = width - x_tile_count * TILE_SIZE;
1409 let remainder_y = height - y_tile_count * TILE_SIZE;
1410
1411 // Process complete tiles
1412 for y_tile in 0..y_tile_count {
1413 for x_tile in 0..x_tile_count {
1414 let x_start = x_tile * TILE_SIZE;
1415 let y_start = y_tile * TILE_SIZE;
1416
1417 unsafe {
1418 transpose_tile_16x16_neon_8b(input, output, width, height, x_start, y_start);
1419 }
1420 }
1421
1422 // Right edge remainder
1423 if remainder_x > 0 {
1424 unsafe {
1425 transpose_block_scalar_8b(
1426 input,
1427 output,
1428 width,
1429 height,
1430 x_tile_count * TILE_SIZE,
1431 y_tile * TILE_SIZE,
1432 remainder_x,
1433 TILE_SIZE,
1434 );
1435 }
1436 }
1437 }
1438
1439 // Bottom edge remainder
1440 if remainder_y > 0 {
1441 for x_tile in 0..x_tile_count {
1442 unsafe {
1443 transpose_block_scalar_8b(
1444 input,
1445 output,
1446 width,
1447 height,
1448 x_tile * TILE_SIZE,
1449 y_tile_count * TILE_SIZE,
1450 TILE_SIZE,
1451 remainder_y,
1452 );
1453 }
1454 }
1455
1456 // Bottom-right corner
1457 if remainder_x > 0 {
1458 unsafe {
1459 transpose_block_scalar_8b(
1460 input,
1461 output,
1462 width,
1463 height,
1464 x_tile_count * TILE_SIZE,
1465 y_tile_count * TILE_SIZE,
1466 remainder_x,
1467 remainder_y,
1468 );
1469 }
1470 }
1471 }
1472}
1473
1474/// Recursive cache-oblivious transpose for large matrices of 8-byte elements.
1475///
1476/// # Safety
1477///
1478/// Caller must ensure valid pointers and that coordinate ranges are within bounds.
1479#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1480#[allow(clippy::too_many_arguments)]
1481unsafe fn transpose_recursive_8b(
1482 input: *const u64,
1483 output: *mut u64,
1484 row_start: usize,
1485 row_end: usize,
1486 col_start: usize,
1487 col_end: usize,
1488 total_cols: usize,
1489 total_rows: usize,
1490) {
1491 let nbr_rows = row_end - row_start;
1492 let nbr_cols = col_end - col_start;
1493
1494 if (nbr_rows <= RECURSIVE_LIMIT && nbr_cols <= RECURSIVE_LIMIT)
1495 || nbr_rows <= 2
1496 || nbr_cols <= 2
1497 {
1498 unsafe {
1499 transpose_region_tiled_8b(
1500 input, output, row_start, row_end, col_start, col_end, total_cols, total_rows,
1501 );
1502 }
1503 return;
1504 }
1505
1506 if nbr_rows >= nbr_cols {
1507 let mid = row_start + (nbr_rows / 2);
1508
1509 unsafe {
1510 transpose_recursive_8b(
1511 input, output, row_start, mid, col_start, col_end, total_cols, total_rows,
1512 );
1513 }
1514
1515 unsafe {
1516 transpose_recursive_8b(
1517 input, output, mid, row_end, col_start, col_end, total_cols, total_rows,
1518 );
1519 }
1520 } else {
1521 let mid = col_start + (nbr_cols / 2);
1522
1523 unsafe {
1524 transpose_recursive_8b(
1525 input, output, row_start, row_end, col_start, mid, total_cols, total_rows,
1526 );
1527 }
1528
1529 unsafe {
1530 transpose_recursive_8b(
1531 input, output, row_start, row_end, mid, col_end, total_cols, total_rows,
1532 );
1533 }
1534 }
1535}
1536
1537/// Tiled transpose for a rectangular region of 8-byte elements.
1538///
1539/// Used as the base case of recursive transpose and for parallel stripe processing.
1540///
1541/// # Safety
1542///
1543/// Caller must ensure:
1544/// - Valid pointers for `total_cols * total_rows` elements
1545/// - `row_start < row_end <= total_rows`
1546/// - `col_start < col_end <= total_cols`
1547#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1548#[inline]
1549#[allow(clippy::too_many_arguments)]
1550unsafe fn transpose_region_tiled_8b(
1551 input: *const u64,
1552 output: *mut u64,
1553 row_start: usize,
1554 row_end: usize,
1555 col_start: usize,
1556 col_end: usize,
1557 total_cols: usize,
1558 total_rows: usize,
1559) {
1560 let nbr_cols = col_end - col_start;
1561 let nbr_rows = row_end - row_start;
1562
1563 let x_tile_count = nbr_cols / TILE_SIZE;
1564 let y_tile_count = nbr_rows / TILE_SIZE;
1565
1566 let remainder_x = nbr_cols - x_tile_count * TILE_SIZE;
1567 let remainder_y = nbr_rows - y_tile_count * TILE_SIZE;
1568
1569 // Process complete tiles
1570 for y_tile in 0..y_tile_count {
1571 for x_tile in 0..x_tile_count {
1572 let col = col_start + x_tile * TILE_SIZE;
1573 let row = row_start + y_tile * TILE_SIZE;
1574
1575 // Uses the buffered tile function: for large matrices the output
1576 // is likely in L3/RAM, so L1 buffering + write prefetching avoids
1577 // RFO stalls on scattered output writes.
1578 unsafe {
1579 transpose_tile_16x16_neon_8b_buffered(
1580 input, output, total_cols, total_rows, col, row,
1581 );
1582 }
1583 }
1584
1585 // Right edge remainder
1586 if remainder_x > 0 {
1587 unsafe {
1588 transpose_block_scalar_8b(
1589 input,
1590 output,
1591 total_cols,
1592 total_rows,
1593 col_start + x_tile_count * TILE_SIZE,
1594 row_start + y_tile * TILE_SIZE,
1595 remainder_x,
1596 TILE_SIZE,
1597 );
1598 }
1599 }
1600 }
1601
1602 // Bottom edge remainder
1603 if remainder_y > 0 {
1604 for x_tile in 0..x_tile_count {
1605 unsafe {
1606 transpose_block_scalar_8b(
1607 input,
1608 output,
1609 total_cols,
1610 total_rows,
1611 col_start + x_tile * TILE_SIZE,
1612 row_start + y_tile_count * TILE_SIZE,
1613 TILE_SIZE,
1614 remainder_y,
1615 );
1616 }
1617 }
1618
1619 // Bottom-right corner
1620 if remainder_x > 0 {
1621 unsafe {
1622 transpose_block_scalar_8b(
1623 input,
1624 output,
1625 total_cols,
1626 total_rows,
1627 col_start + x_tile_count * TILE_SIZE,
1628 row_start + y_tile_count * TILE_SIZE,
1629 remainder_x,
1630 remainder_y,
1631 );
1632 }
1633 }
1634 }
1635}
1636
1637/// Transpose a complete 16×16 tile of 8-byte elements using NEON SIMD (direct-to-output).
1638///
1639/// Used by the **medium tiled path** where the output likely fits in L2 cache.
1640///
1641/// # Safety
1642///
1643/// Caller must ensure:
1644/// - Valid pointers for the full matrix
1645/// - `x_start + 16 <= width`
1646/// - `y_start + 16 <= height`
1647#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1648#[inline]
1649unsafe fn transpose_tile_16x16_neon_8b(
1650 input: *const u64,
1651 output: *mut u64,
1652 width: usize,
1653 height: usize,
1654 x_start: usize,
1655 y_start: usize,
1656) {
1657 unsafe {
1658 // Block Row 0 (input rows y_start..y_start+4)
1659 let inp = input.add(y_start * width + x_start);
1660 let out = output.add(x_start * height + y_start);
1661 transpose_4x4_neon_8b(inp, out, width, height);
1662 transpose_4x4_neon_8b(inp.add(4), out.add(4 * height), width, height);
1663 transpose_4x4_neon_8b(inp.add(8), out.add(8 * height), width, height);
1664 transpose_4x4_neon_8b(inp.add(12), out.add(12 * height), width, height);
1665
1666 // Block Row 1 (input rows y_start+4..y_start+8)
1667 let inp = input.add((y_start + 4) * width + x_start);
1668 let out = output.add(x_start * height + y_start + 4);
1669 transpose_4x4_neon_8b(inp, out, width, height);
1670 transpose_4x4_neon_8b(inp.add(4), out.add(4 * height), width, height);
1671 transpose_4x4_neon_8b(inp.add(8), out.add(8 * height), width, height);
1672 transpose_4x4_neon_8b(inp.add(12), out.add(12 * height), width, height);
1673
1674 // Block Row 2 (input rows y_start+8..y_start+12)
1675 let inp = input.add((y_start + 8) * width + x_start);
1676 let out = output.add(x_start * height + y_start + 8);
1677 transpose_4x4_neon_8b(inp, out, width, height);
1678 transpose_4x4_neon_8b(inp.add(4), out.add(4 * height), width, height);
1679 transpose_4x4_neon_8b(inp.add(8), out.add(8 * height), width, height);
1680 transpose_4x4_neon_8b(inp.add(12), out.add(12 * height), width, height);
1681
1682 // Block Row 3 (input rows y_start+12..y_start+16)
1683 let inp = input.add((y_start + 12) * width + x_start);
1684 let out = output.add(x_start * height + y_start + 12);
1685 transpose_4x4_neon_8b(inp, out, width, height);
1686 transpose_4x4_neon_8b(inp.add(4), out.add(4 * height), width, height);
1687 transpose_4x4_neon_8b(inp.add(8), out.add(8 * height), width, height);
1688 transpose_4x4_neon_8b(inp.add(12), out.add(12 * height), width, height);
1689 }
1690}
1691
1692/// Transpose a complete 16×16 tile of 8-byte elements with L1 buffering.
1693///
1694/// Used by the **recursive/parallel path** for large matrices where the output
1695/// is in L3/RAM. L1 buffering + write prefetching avoids RFO stalls.
1696///
1697/// # Safety
1698///
1699/// Caller must ensure:
1700/// - Valid pointers for the full matrix
1701/// - `x_start + 16 <= width`
1702/// - `y_start + 16 <= height`
1703#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1704#[inline]
1705unsafe fn transpose_tile_16x16_neon_8b_buffered(
1706 input: *const u64,
1707 output: *mut u64,
1708 width: usize,
1709 height: usize,
1710 x_start: usize,
1711 y_start: usize,
1712) {
1713 // Stack buffer for L1-hot transpose (2 KB for u64).
1714 let mut buffer = MaybeUninit::<[u64; TILE_SIZE * TILE_SIZE]>::uninit();
1715 let buf = buffer.as_mut_ptr().cast::<u64>();
1716
1717 unsafe {
1718 // Transpose 4×4 grid of NEON blocks into the buffer.
1719
1720 // Block Row 0 (input rows y_start..y_start+4)
1721 let inp = input.add(y_start * width + x_start);
1722 transpose_4x4_neon_8b(inp, buf, width, TILE_SIZE);
1723 transpose_4x4_neon_8b(inp.add(4), buf.add(4 * TILE_SIZE), width, TILE_SIZE);
1724 transpose_4x4_neon_8b(inp.add(8), buf.add(8 * TILE_SIZE), width, TILE_SIZE);
1725 transpose_4x4_neon_8b(inp.add(12), buf.add(12 * TILE_SIZE), width, TILE_SIZE);
1726
1727 // Block Row 1 (input rows y_start+4..y_start+8)
1728 let inp = input.add((y_start + 4) * width + x_start);
1729 transpose_4x4_neon_8b(inp, buf.add(4), width, TILE_SIZE);
1730 transpose_4x4_neon_8b(inp.add(4), buf.add(4 * TILE_SIZE + 4), width, TILE_SIZE);
1731 transpose_4x4_neon_8b(inp.add(8), buf.add(8 * TILE_SIZE + 4), width, TILE_SIZE);
1732 transpose_4x4_neon_8b(inp.add(12), buf.add(12 * TILE_SIZE + 4), width, TILE_SIZE);
1733
1734 // Block Row 2 (input rows y_start+8..y_start+12)
1735 let inp = input.add((y_start + 8) * width + x_start);
1736 transpose_4x4_neon_8b(inp, buf.add(8), width, TILE_SIZE);
1737 transpose_4x4_neon_8b(inp.add(4), buf.add(4 * TILE_SIZE + 8), width, TILE_SIZE);
1738 transpose_4x4_neon_8b(inp.add(8), buf.add(8 * TILE_SIZE + 8), width, TILE_SIZE);
1739 transpose_4x4_neon_8b(inp.add(12), buf.add(12 * TILE_SIZE + 8), width, TILE_SIZE);
1740
1741 // Block Row 3 (input rows y_start+12..y_start+16)
1742 let inp = input.add((y_start + 12) * width + x_start);
1743 transpose_4x4_neon_8b(inp, buf.add(12), width, TILE_SIZE);
1744 transpose_4x4_neon_8b(inp.add(4), buf.add(4 * TILE_SIZE + 12), width, TILE_SIZE);
1745 transpose_4x4_neon_8b(inp.add(8), buf.add(8 * TILE_SIZE + 12), width, TILE_SIZE);
1746 transpose_4x4_neon_8b(inp.add(12), buf.add(12 * TILE_SIZE + 12), width, TILE_SIZE);
1747
1748 // Flush buffer to output with write prefetching.
1749 prefetch_write(output.add(x_start * height + y_start) as *const u8);
1750 for c in 0..TILE_SIZE {
1751 if c + 1 < TILE_SIZE {
1752 prefetch_write(output.add((x_start + c + 1) * height + y_start) as *const u8);
1753 }
1754 core::ptr::copy_nonoverlapping(
1755 buf.add(c * TILE_SIZE),
1756 output.add((x_start + c) * height + y_start),
1757 TILE_SIZE,
1758 );
1759 }
1760 }
1761}
1762
1763/// Scalar transpose for an arbitrary rectangular block of 8-byte elements.
1764///
1765/// Used for handling edge cases where dimensions don't align to tile boundaries.
1766///
1767/// # Safety
1768///
1769/// Caller must ensure:
1770/// - Valid pointers for the full matrix
1771/// - `x_start + block_width <= width`
1772/// - `y_start + block_height <= height`
1773#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1774#[inline]
1775#[allow(clippy::too_many_arguments)]
1776unsafe fn transpose_block_scalar_8b(
1777 input: *const u64,
1778 output: *mut u64,
1779 width: usize,
1780 height: usize,
1781 x_start: usize,
1782 y_start: usize,
1783 block_width: usize,
1784 block_height: usize,
1785) {
1786 for inner_x in 0..block_width {
1787 for inner_y in 0..block_height {
1788 let x = x_start + inner_x;
1789 let y = y_start + inner_y;
1790
1791 let input_index = x + y * width;
1792 let output_index = y + x * height;
1793
1794 unsafe {
1795 *output.add(output_index) = *input.add(input_index);
1796 }
1797 }
1798 }
1799}
1800
1801/// Transpose a 4×4 block of 64-bit elements using NEON SIMD.
1802///
1803/// This is the fundamental building block for 8-byte element transpose.
1804///
1805/// Since a 128-bit NEON register holds only 2 u64 elements, each row of 4
1806/// elements requires 2 registers. A 4×4 block uses 8 registers for input
1807/// and 8 for output (16 total, well within NEON's 32 registers).
1808///
1809/// # Algorithm
1810///
1811/// The transpose uses a single-stage butterfly on four independent 2×2
1812/// sub-blocks:
1813///
1814/// ```text
1815/// Load: q0_lo=[a00,a01] q0_hi=[a02,a03] (row 0)
1816/// q1_lo=[a10,a11] q1_hi=[a12,a13] (row 1)
1817/// q2_lo=[a20,a21] q2_hi=[a22,a23] (row 2)
1818/// q3_lo=[a30,a31] q3_hi=[a32,a33] (row 3)
1819///
1820/// Transpose 2×2 sub-blocks:
1821/// Top-left: trn1(q0_lo,q1_lo)=[a00,a10] trn2(q0_lo,q1_lo)=[a01,a11]
1822/// Top-right: trn1(q0_hi,q1_hi)=[a02,a12] trn2(q0_hi,q1_hi)=[a03,a13]
1823/// Bottom-left: trn1(q2_lo,q3_lo)=[a20,a30] trn2(q2_lo,q3_lo)=[a21,a31]
1824/// Bottom-right: trn1(q2_hi,q3_hi)=[a22,a32] trn2(q2_hi,q3_hi)=[a23,a33]
1825///
1826/// Store: row0=[a00,a10,a20,a30] row1=[a01,a11,a21,a31]
1827/// row2=[a02,a12,a22,a32] row3=[a03,a13,a23,a33]
1828/// ```
1829///
1830/// # Safety
1831///
1832/// Caller must ensure:
1833/// - `src` is valid for reading 4 rows of `src_stride` elements each
1834/// - `dst` is valid for writing 4 rows of `dst_stride` elements each
1835/// - The first 4 elements of each row are accessible
1836#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1837#[inline(always)]
1838unsafe fn transpose_4x4_neon_8b(
1839 src: *const u64,
1840 dst: *mut u64,
1841 src_stride: usize,
1842 dst_stride: usize,
1843) {
1844 unsafe {
1845 // Load 4 rows, 2 registers per row (4 u64 = 2 × 128-bit)
1846
1847 // Row 0: [a00, a01] [a02, a03]
1848 let q0_lo = vld1q_u64(src);
1849 let q0_hi = vld1q_u64(src.add(2));
1850 // Row 1: [a10, a11] [a12, a13]
1851 let q1_lo = vld1q_u64(src.add(src_stride));
1852 let q1_hi = vld1q_u64(src.add(src_stride + 2));
1853 // Row 2: [a20, a21] [a22, a23]
1854 let q2_lo = vld1q_u64(src.add(2 * src_stride));
1855 let q2_hi = vld1q_u64(src.add(2 * src_stride + 2));
1856 // Row 3: [a30, a31] [a32, a33]
1857 let q3_lo = vld1q_u64(src.add(3 * src_stride));
1858 let q3_hi = vld1q_u64(src.add(3 * src_stride + 2));
1859
1860 // Transpose four 2×2 sub-blocks using vtrn1q_u64/vtrn2q_u64
1861
1862 // Top-left: rows 0,1 × columns 0,1
1863 let r0_lo = vtrn1q_u64(q0_lo, q1_lo); // [a00, a10]
1864 let r1_lo = vtrn2q_u64(q0_lo, q1_lo); // [a01, a11]
1865 // Top-right: rows 0,1 × columns 2,3
1866 let r2_lo = vtrn1q_u64(q0_hi, q1_hi); // [a02, a12]
1867 let r3_lo = vtrn2q_u64(q0_hi, q1_hi); // [a03, a13]
1868 // Bottom-left: rows 2,3 × columns 0,1
1869 let r0_hi = vtrn1q_u64(q2_lo, q3_lo); // [a20, a30]
1870 let r1_hi = vtrn2q_u64(q2_lo, q3_lo); // [a21, a31]
1871 // Bottom-right: rows 2,3 × columns 2,3
1872 let r2_hi = vtrn1q_u64(q2_hi, q3_hi); // [a22, a32]
1873 let r3_hi = vtrn2q_u64(q2_hi, q3_hi); // [a23, a33]
1874
1875 // Store 4 transposed rows, 2 registers per row
1876
1877 // Row 0: [a00, a10, a20, a30]
1878 vst1q_u64(dst, r0_lo);
1879 vst1q_u64(dst.add(2), r0_hi);
1880 // Row 1: [a01, a11, a21, a31]
1881 vst1q_u64(dst.add(dst_stride), r1_lo);
1882 vst1q_u64(dst.add(dst_stride + 2), r1_hi);
1883 // Row 2: [a02, a12, a22, a32]
1884 vst1q_u64(dst.add(2 * dst_stride), r2_lo);
1885 vst1q_u64(dst.add(2 * dst_stride + 2), r2_hi);
1886 // Row 3: [a03, a13, a23, a33]
1887 vst1q_u64(dst.add(3 * dst_stride), r3_lo);
1888 vst1q_u64(dst.add(3 * dst_stride + 2), r3_hi);
1889 }
1890}
1891
1892#[cfg(test)]
1893mod tests {
1894 use alloc::vec;
1895 use alloc::vec::Vec;
1896
1897 use p3_baby_bear::BabyBear;
1898 use p3_field::PrimeCharacteristicRing;
1899 use p3_goldilocks::Goldilocks;
1900 use proptest::prelude::*;
1901
1902 use super::*;
1903
1904 /// A type with the same size/alignment mismatch as `Complex<Mersenne31>`:
1905 /// 8 bytes, but only 4-byte aligned. Must not be routed through the 8-byte
1906 /// NEON path, which assumes `u64` alignment.
1907 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1908 #[repr(C, align(4))]
1909 struct Size8Align4([u8; 8]);
1910
1911 /// Naive reference implementation for correctness testing.
1912 fn transpose_reference<T: Copy + Default>(input: &[T], width: usize, height: usize) -> Vec<T> {
1913 // Allocate output buffer with same size as input.
1914 let mut output = vec![T::default(); width * height];
1915
1916 // For each position (x, y) in the input matrix:
1917 // - Input index: y * width + x (row-major)
1918 // - Output index: x * height + y (transposed row-major)
1919 for y in 0..height {
1920 for x in 0..width {
1921 output[x * height + y] = input[y * width + x];
1922 }
1923 }
1924
1925 output
1926 }
1927
1928 /// Strategy for generating matrix dimensions.
1929 fn dimension_strategy() -> impl Strategy<Value = (usize, usize)> {
1930 // Compute boundary dimensions from constants.
1931 // `small_side` is the largest square that stays in the small (scalar) path.
1932 let small_side = (SMALL_LEN as f64).sqrt() as usize;
1933 // `medium_side` is the largest square that stays in the medium (tiled) path.
1934 let medium_side = (MEDIUM_LEN as f64).sqrt() as usize;
1935 // `large_side` is the side length that triggers the large (recursive) path.
1936 let large_side = medium_side + 1;
1937
1938 prop_oneof![
1939 // Edge cases: empty and degenerate matrices
1940 //
1941 // Empty matrix (0×0)
1942 Just((0, 0)),
1943 // Single row (1×n) - tests degenerate case
1944 (1..=100_usize).prop_map(|w| (w, 1)),
1945 // Single column (n×1) - tests degenerate case
1946 (1..=100_usize).prop_map(|h| (1, h)),
1947 // Small path: len < SMALL_LEN (scalar transpose)
1948 //
1949 // These dimensions exercise the scalar transpose path.
1950
1951 // Tiny matrices (various shapes within small threshold)
1952 (1..=small_side, 1..=small_side),
1953 // Medium path: SMALL_LEN ≤ len < MEDIUM_LEN (tiled TILE_SIZE×TILE_SIZE)
1954 //
1955 // These dimensions exercise the tiled TILE_SIZE×TILE_SIZE path.
1956
1957 // Exactly 4×4 (single NEON block)
1958 Just((4, 4)),
1959 // Exactly TILE_SIZE×TILE_SIZE (single tile)
1960 Just((TILE_SIZE, TILE_SIZE)),
1961 // Multiple complete tiles (2× and 4× TILE_SIZE)
1962 Just((TILE_SIZE * 2, TILE_SIZE * 2)),
1963 Just((TILE_SIZE * 4, TILE_SIZE * 4)),
1964 // Non-aligned: has remainders in both dimensions
1965 // Range from just above TILE_SIZE to below 4×TILE_SIZE.
1966 // These test the scalar fallback for tile edges.
1967 (
1968 (TILE_SIZE + 1)..=(TILE_SIZE * 4 - 1),
1969 (TILE_SIZE + 1)..=(TILE_SIZE * 4 - 1)
1970 ),
1971 // Wide rectangle with remainders (medium path)
1972 (50..=200_usize, 10..=50_usize),
1973 // Tall rectangle with remainders (medium path)
1974 (10..=50_usize, 50..=200_usize),
1975 // Large path: MEDIUM_LEN ≤ len < PARALLEL_THRESHOLD (recursive)
1976 //
1977 // These exercise the cache-oblivious recursive subdivision.
1978
1979 // Square matrices triggering recursion (just above medium threshold)
1980 Just((large_side, large_side)),
1981 // Slightly larger square
1982 Just((large_side + 100, large_side + 100)),
1983 // Wide rectangle triggering recursion
1984 Just((large_side * 2, large_side / 2)),
1985 // Tall rectangle triggering recursion
1986 Just((large_side / 2, large_side * 2)),
1987 // Non-power-of-2 dimensions in large range
1988 Just((large_side + 50, large_side + 75)),
1989 ]
1990 }
1991
1992 proptest! {
1993 #[test]
1994 fn proptest_transpose_babybear((width, height) in dimension_strategy()) {
1995 // Skip empty matrices (they're trivially correct).
1996 if width == 0 || height == 0 {
1997 // Just verify it doesn't panic.
1998 let input: [BabyBear; 0] = [];
1999 let mut output: [BabyBear; 0] = [];
2000 transpose(&input, &mut output, width, height);
2001 return Ok(());
2002 }
2003
2004 // Create input matrix with unique values at each position.
2005 let input: Vec<BabyBear> = (0..width * height)
2006 .map(|i| BabyBear::from_u64(i as u64))
2007 .collect();
2008
2009 // Allocate output buffer.
2010 let mut output = BabyBear::zero_vec(width * height);
2011
2012 // Run optimized transpose.
2013 transpose(&input, &mut output, width, height);
2014
2015 // Run reference transpose.
2016 let expected = transpose_reference(&input, width, height);
2017
2018 // Verify results match.
2019 prop_assert_eq!(
2020 output,
2021 expected,
2022 "Transpose mismatch for {}×{} matrix",
2023 width,
2024 height
2025 );
2026 }
2027
2028 #[test]
2029 fn proptest_transpose_u64((width, height) in dimension_strategy()) {
2030 // Skip empty and very large matrices for u64 (memory intensive).
2031 if width == 0 || height == 0 || width * height > 100_000 {
2032 return Ok(());
2033 }
2034
2035 // Create input with unique values.
2036 let input: Vec<u64> = (0..width * height).map(|i| i as u64).collect();
2037
2038 // Allocate output.
2039 let mut output = vec![0u64; width * height];
2040
2041 // Run transpose.
2042 transpose(&input, &mut output, width, height);
2043
2044 // Verify against reference.
2045 let expected = transpose_reference(&input, width, height);
2046 prop_assert_eq!(output, expected);
2047 }
2048
2049 #[test]
2050 fn proptest_transpose_u8((width, height) in dimension_strategy()) {
2051 // Skip empty and very large matrices.
2052 if width == 0 || height == 0 || width * height > 100_000 {
2053 return Ok(());
2054 }
2055
2056 // Create input with unique values (wrapping for u8).
2057 let input: Vec<u8> = (0..width * height).map(|i| i as u8).collect();
2058
2059 // Allocate output.
2060 let mut output = vec![0u8; width * height];
2061
2062 // Run transpose.
2063 transpose(&input, &mut output, width, height);
2064
2065 // Verify against reference.
2066 let expected = transpose_reference(&input, width, height);
2067 prop_assert_eq!(output, expected);
2068 }
2069
2070 #[test]
2071 fn proptest_transpose_size8_align4((width, height) in dimension_strategy()) {
2072 // Skip empty and very large matrices.
2073 if width == 0 || height == 0 || width * height > 100_000 {
2074 return Ok(());
2075 }
2076
2077 // Create input with unique values.
2078 let input: Vec<Size8Align4> = (0..width * height)
2079 .map(|i| Size8Align4((i as u64).to_le_bytes()))
2080 .collect();
2081
2082 // Allocate output.
2083 let mut output = vec![Size8Align4::default(); width * height];
2084
2085 // Run transpose.
2086 transpose(&input, &mut output, width, height);
2087
2088 // Verify against reference.
2089 let expected = transpose_reference(&input, width, height);
2090 prop_assert_eq!(output, expected);
2091 }
2092
2093 #[test]
2094 fn proptest_transpose_goldilocks((width, height) in dimension_strategy()) {
2095 // Skip empty matrices.
2096 if width == 0 || height == 0 {
2097 let input: [Goldilocks; 0] = [];
2098 let mut output: [Goldilocks; 0] = [];
2099 transpose(&input, &mut output, width, height);
2100 return Ok(());
2101 }
2102
2103 // Create input matrix with unique values at each position.
2104 let input: Vec<Goldilocks> = (0..width * height)
2105 .map(|i| Goldilocks::from_u64(i as u64))
2106 .collect();
2107
2108 // Allocate output buffer.
2109 let mut output = Goldilocks::zero_vec(width * height);
2110
2111 // Run optimized transpose.
2112 transpose(&input, &mut output, width, height);
2113
2114 // Run reference transpose.
2115 let expected = transpose_reference(&input, width, height);
2116
2117 // Verify results match.
2118 prop_assert_eq!(
2119 output,
2120 expected,
2121 "Transpose mismatch for {}×{} matrix",
2122 width,
2123 height
2124 );
2125 }
2126 }
2127
2128 #[test]
2129 fn transpose_parallel_paths_match_reference() {
2130 // The longer-dimension striping runs only past the parallel threshold,
2131 // and only on aarch64 with the `parallel` feature.
2132 //
2133 // The proptest dimensions stay below that threshold, so these shapes
2134 // cross it on purpose to cover both stripings.
2135 let shapes = [
2136 (1 << 13, 640), // wide, tile-aligned
2137 (640, 1 << 13), // tall, tile-aligned
2138 (8191, 641), // wide, off both tile and thread boundaries
2139 (641, 8191), // tall, off both tile and thread boundaries
2140 (1 << 22, 2), // degenerate wide: a two-row input
2141 ];
2142 for (width, height) in shapes {
2143 // Distinct values 0..size, so a misplaced element is caught.
2144 let size = width * height;
2145
2146 // 4-byte path.
2147 let input: Vec<u32> = (0..size as u32).collect();
2148 let mut output = vec![0u32; size];
2149 transpose(&input, &mut output, width, height);
2150 assert_eq!(
2151 output,
2152 transpose_reference(&input, width, height),
2153 "4-byte parallel transpose mismatch for {width}×{height}"
2154 );
2155
2156 // 8-byte path.
2157 let input: Vec<u64> = (0..size as u64).collect();
2158 let mut output = vec![0u64; size];
2159 transpose(&input, &mut output, width, height);
2160 assert_eq!(
2161 output,
2162 transpose_reference(&input, width, height),
2163 "8-byte parallel transpose mismatch for {width}×{height}"
2164 );
2165 }
2166 }
2167}