lightweight_simd/lib.rs
1// Most or all contents generated by AI (model glm-5.3)
2
3//! Lightweight generic fixed-array vector types.
4//!
5//! This crate provides [`Simd`], a fixed-lane vector type `Simd<T, N>` over
6//! `[T; N]`, with lane-wise arithmetic (vector/vector and vector/scalar),
7//! multiply-add (fused where the target has hardware FMA; see below), float
8//! functions, comparisons producing masks, mask selection, horizontal
9//! reductions, and slice load/store — all implemented without intrinsics and
10//! without runtime instruction dispatch.
11//!
12//! Performance relies on the compiler auto-vectorizing this portable code;
13//! build with `-C target-cpu=native` (or a similar `target-cpu` setting) to
14//! obtain SIMD-level efficiency. Correctness does not depend on it.
15//!
16//! Element categories are expressed by trait bounds rather than by distinct
17//! types: arithmetic requires [`num_traits::Num`], the float functions require
18//! [`num_traits::Float`], multiply-add requires [`num_traits::MulAdd`] where
19//! the target has hardware FMA and [`core::ops::Mul`] with [`core::ops::Add`]
20//! otherwise, and bitwise operations require the element's own
21//! [`core::ops`] implementations. `bool` is a valid element, and
22//! `Simd<bool, N>` doubles as the mask type. The lane count `N` is an
23//! arbitrary const generic.
24//!
25//! A full one-file map of the API surface — written for users and AI coding
26//! agents — lives in the [`guide`] module (kept as `src/guide.md` in the
27//! repository).
28//!
29//! Concrete aliases such as `f64x8` are enforced-alignment types
30//! ([`Aligned64<f64, 8>`](Aligned64)): array-backed siblings of [`Simd`] that
31//! share one implementation — every operation is defined once for `Simd` and
32//! the five aligned types together. The alignment is the vector's byte size
33//! rounded up to a power of two, capped at 64 bytes; conversions between plain
34//! and aligned vectors are `From`/`.0`/`to_simd`. Masks (`Simd<bool, N>` and
35//! the `maskN` aliases) carry no enforced alignment; per-lane selection is
36//! [`mask_select`](Simd::mask_select) on the value (available on every vector
37//! type), or [`Simd::select`] on the mask for plain vectors.
38//!
39//! # Multiply-add semantics
40//!
41//! [`mul_add`](Simd::mul_add) and [`fma_from`](Simd::fma_from) follow the
42//! compilation target (see `docs/adr/0005` in the repository for the
43//! measured trade-off):
44//!
45//! - **targets with hardware FMA** — x86 built with `-C target-cpu` of `x86-64-v3` or higher
46//! (including `native`), and all aarch64: the element's fused [`num_traits::MulAdd`], a single
47//! rounding per lane, one FMA instruction per register, no extra flags.
48//! - **targets without** — generic x86-64, `x86-64-v2`, and architectures the predicate does not
49//! know: a separated multiply and add (two roundings per lane) that never degrades into the slow
50//! software-fused libm `fma` calls (~4.6x slower than separated there). Under `-C
51//! llvm-args=-fp-contract=fast` this form still fuses into a hardware FMA instruction on capable
52//! targets.
53//! - the non-default cargo feature **`use_libm_fma`** pins the fused form on *every* target,
54//! accepting the libm software `fma` where hardware is absent — use it when results must be
55//! bit-identical across build targets.
56//!
57//! # Example
58//!
59//! Same example as the README; kept as a doctest so it is verified by CI.
60//!
61//! ```
62//! use lightweight_simd::{f64x8, Simd};
63//!
64//! // typed aliases (enforced alignment) or the generic Simd<T, N>
65//! let a = f64x8::from([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
66//! let b = f64x8::splat(2.0);
67//!
68//! let c = a.mul_add(b, f64x8::one()); // a*b + 1, fused on FMA targets
69//! assert_eq!(c.reduce_sum(), 80.0);
70//!
71//! let mask = a.simd_gt(f64x8::splat(4.0)); // comparisons -> masks
72//! assert_eq!(a.mask_select(mask, b).to_array(), [2.0, 2.0, 2.0, 2.0, 5.0, 6.0, 7.0, 8.0]);
73//!
74//! let mut buf = [0.0; 8];
75//! a.store_slice(&mut buf); // slice load/store
76//!
77//! // any element type and lane count
78//! let u = Simd::from([10u32, 20, 30]);
79//! assert_eq!((u * 3).to_array(), [30, 60, 90]);
80//! ```
81
82#![warn(missing_docs)]
83#![deny(unsafe_op_in_unsafe_fn)]
84
85use core::ops::{Index, IndexMut};
86use num_traits::{Num, One, Zero};
87
88mod alias;
89pub mod aligned;
90mod memory;
91mod ops;
92mod reduce;
93
94/// Dummy doc-only module: renders `src/guide.md` (the one-file API overview,
95/// also linked from the README) into the rustdoc output.
96#[cfg(doc)]
97pub mod guide {
98 #![doc = include_str!("guide.md")]
99}
100
101pub use alias::*;
102pub use aligned::{Aligned4, Aligned8, Aligned16, Aligned32, Aligned64};
103
104/// A fixed-length vector of `N` lanes of element type `T`.
105///
106/// All operations are lane-wise and implemented as plain loops over the
107/// underlying `[T; N]`, so the compiler can auto-vectorize them for any
108/// element type and lane count. The lane count `N` is arbitrary (powers of two
109/// that fill whole SIMD registers are the sweet spot, but not required).
110///
111/// The type carries no enforced alignment beyond the element's natural
112/// alignment; for enforced alignment use the [`aligned`] sibling types, which
113/// share this type's full operation surface. The field is public for
114/// zero-friction interoperability with array code.
115#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)]
116pub struct Simd<T, const N: usize>(pub [T; N]);
117
118#[duplicate::duplicate_item(
119 _mod_ _vec_;
120 [simd_impls] [Simd];
121 [impls_a4] [Aligned4];
122 [impls_a8] [Aligned8];
123 [impls_a16] [Aligned16];
124 [impls_a32] [Aligned32];
125 [impls_a64] [Aligned64];
126)]
127mod _mod_ {
128 use super::*;
129
130 /* #region constructors and access */
131
132 impl<T, const N: usize> _vec_<T, N> {
133 /// Returns a vector whose lane `i` is `f(i)`.
134 #[inline(always)]
135 pub fn from_fn<F>(f: F) -> Self
136 where
137 F: FnMut(usize) -> T,
138 {
139 Self(core::array::from_fn(f))
140 }
141
142 /// Applies `f` to each lane, returning a new vector.
143 ///
144 /// This is the generic escape hatch for operations the crate does not
145 /// provide (e.g. transcendental functions: `v.map(f64::exp)`).
146 #[inline(always)]
147 pub fn map<F>(self, f: F) -> Self
148 where
149 F: FnMut(T) -> T,
150 {
151 Self(self.0.map(f))
152 }
153
154 /// Returns the underlying array by reference.
155 #[inline(always)]
156 pub const fn as_array(&self) -> &[T; N] {
157 &self.0
158 }
159
160 /// Returns the underlying array by mutable reference.
161 #[inline(always)]
162 pub fn as_array_mut(&mut self) -> &mut [T; N] {
163 &mut self.0
164 }
165
166 /// Returns the underlying array by value.
167 #[inline(always)]
168 pub fn to_array(self) -> [T; N] {
169 self.0
170 }
171 }
172
173 impl<T: Copy, const N: usize> _vec_<T, N> {
174 /// Returns a vector with all lanes set to `val`.
175 #[inline(always)]
176 pub const fn splat(val: T) -> Self {
177 Self([val; N])
178 }
179
180 /// Sets all lanes to `val`.
181 #[inline(always)]
182 pub fn fill(&mut self, val: T) {
183 self.0 = [val; N];
184 }
185
186 /// Per-lane select: keeps lanes of `self` where `mask` is `true` and
187 /// lanes of `other` where it is `false`. This value-side form is
188 /// available on every vector type; the mask-side
189 /// [`select`](Simd::select) applies to plain vectors.
190 #[inline(always)]
191 pub fn mask_select(self, mask: Simd<bool, N>, other: Self) -> Self {
192 let mut out = self.0;
193 for ((o, f), m) in out.iter_mut().zip(other.0).zip(mask.0) {
194 if !m {
195 *o = f;
196 }
197 }
198 Self(out)
199 }
200
201 /// Returns an uninitialized vector, intended for scratch buffers whose
202 /// lanes are fully written before being read.
203 ///
204 /// # Safety
205 ///
206 /// The caller must initialize all lanes before any read. The element
207 /// type must additionally admit arbitrary bit patterns (true for
208 /// floats and integers; not for types with niches such as references).
209 #[inline(always)]
210 #[allow(clippy::uninit_assumed_init)]
211 pub unsafe fn uninit() -> Self {
212 // SAFETY: the caller contract requires full initialization before
213 // any read of the returned value.
214 unsafe { core::mem::MaybeUninit::uninit().assume_init() }
215 }
216 }
217
218 impl<T: Zero + Copy, const N: usize> _vec_<T, N> {
219 /// Returns a vector with all lanes set to zero.
220 #[inline(always)]
221 pub fn zero() -> Self {
222 Self::splat(T::zero())
223 }
224 }
225
226 impl<T: One + Copy, const N: usize> _vec_<T, N> {
227 /// Returns a vector with all lanes set to one.
228 #[inline(always)]
229 pub fn one() -> Self {
230 Self::splat(T::one())
231 }
232 }
233
234 /* #endregion */
235
236 /* #region common traits */
237
238 impl<T: Default, const N: usize> Default for _vec_<T, N> {
239 #[inline(always)]
240 fn default() -> Self {
241 Self(core::array::from_fn(|_| T::default()))
242 }
243 }
244
245 impl<T, const N: usize> From<[T; N]> for _vec_<T, N> {
246 #[inline(always)]
247 fn from(array: [T; N]) -> Self {
248 Self(array)
249 }
250 }
251
252 impl<T, const N: usize> Index<usize> for _vec_<T, N> {
253 type Output = T;
254
255 #[inline(always)]
256 fn index(&self, index: usize) -> &Self::Output {
257 &self.0[index]
258 }
259 }
260
261 impl<T, const N: usize> IndexMut<usize> for _vec_<T, N> {
262 #[inline(always)]
263 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
264 &mut self.0[index]
265 }
266 }
267
268 impl<T: Num + Copy, const N: usize> Zero for _vec_<T, N> {
269 #[inline(always)]
270 fn zero() -> Self {
271 Self::splat(T::zero())
272 }
273
274 #[inline(always)]
275 fn is_zero(&self) -> bool {
276 self.0.iter().all(Zero::is_zero)
277 }
278 }
279
280 impl<T: Num + Copy, const N: usize> One for _vec_<T, N> {
281 #[inline(always)]
282 fn one() -> Self {
283 Self::splat(T::one())
284 }
285 }
286
287 /* #endregion */
288}