Skip to main content

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}