Skip to main content

hermes_simd/dispatch/
complex.rs

1//! Generic interleaved complex kernels.
2//!
3//! The public surface accepts primitive lane slices in `[re, im, ...]` order so
4//! domain crates can use their own complex storage types without Hermes taking a
5//! dependency on a concrete complex-number crate.
6//!
7//! Arithmetic stays in vector registers for every `(T, Arch)` pair via the
8//! adjacent-pair shuffle and alternating-FMA primitives on
9//! [`SimdKernel`]:
10//!
11//! - `a * b`       = `fmaddsub(dup_even(a), b, mul(dup_odd(a), swap_adjacent(b)))`
12//! - `a * conj(b)` = `fmsubadd(dup_odd(a), swap_adjacent(b), mul(dup_even(a), b))`
13//!
14//! Runtime architecture selection is generated by `#[runtime_dispatch]`, the
15//! same mechanism used by the dense kernels — no per-type provider impls and no
16//! `OnceLock` feature caching.
17
18use hermes_simd_core::{arch::SimdArch, kernel::SimdKernel, scalar::Scalar, view::SimdError};
19use hermes_simd_macros::runtime_dispatch;
20
21const MAX_STACK_LANES: usize = 128;
22
23#[inline]
24fn mul_pair<T, const CONJ_B: bool>(ar: T, ai: T, br: T, bi: T) -> (T, T)
25where
26    T: Scalar,
27{
28    if CONJ_B {
29        // a * conj(b): (ar+i·ai)(br-i·bi) = (ar·br + ai·bi) + i·(ai·br - ar·bi)
30        (ar * br + ai * bi, ai * br - ar * bi)
31    } else {
32        // a * b: (ar+i·ai)(br+i·bi) = (ar·br - ai·bi) + i·(ar·bi + ai·br)
33        (ar * br - ai * bi, ar * bi + ai * br)
34    }
35}
36
37/// Computes one register of interleaved complex products: `a[k] * b[k]`
38/// (or `a[k] * conj(b[k])` when `CONJ_B`) over adjacent lane pairs.
39///
40/// # Safety
41/// Processor must support the target features required by `A`.
42#[inline(always)]
43unsafe fn complex_mul_vector<T, A, const CONJ_B: bool>(av: A::Vector, bv: A::Vector) -> A::Vector
44where
45    T: Scalar,
46    A: SimdArch + SimdKernel<T>,
47{
48    let b_sw = A::swap_adjacent(bv);
49    if CONJ_B {
50        // even: ai*bi + ar*br = re ; odd: ai*br - ar*bi = im
51        A::fmsubadd(A::dup_odd(av), b_sw, A::mul(A::dup_even(av), bv))
52    } else {
53        // even: ar*br - ai*bi = re ; odd: ar*bi + ai*br = im
54        A::fmaddsub(A::dup_even(av), bv, A::mul(A::dup_odd(av), b_sw))
55    }
56}
57
58/// Multiplies interleaved complex values in-place using architecture `A`.
59///
60/// `a` and `b` must have identical even lengths. The operation is value
61/// preserving with respect to scalar complex multiplication over adjacent
62/// primitive lane pairs:
63///
64/// `a[k] = a[k] * b[k]` when `CONJ_B == false`, and
65/// `a[k] = a[k] * conj(b[k])` when `CONJ_B == true`.
66///
67/// # Examples
68///
69/// ```
70/// use hermes_simd::{interleaved_complex_mul_assign, Scalar};
71///
72/// let mut lhs = [1.0_f64, 2.0, 3.0, -1.0];
73/// let rhs = [4.0_f64, -2.0, 0.5, 5.0];
74///
75/// interleaved_complex_mul_assign::<f64, Scalar, false>(&mut lhs, &rhs).unwrap();
76///
77/// assert_eq!(lhs, [8.0, 6.0, 6.5, 14.5]);
78/// ```
79#[inline]
80pub fn interleaved_complex_mul_assign<T, A, const CONJ_B: bool>(
81    a: &mut [T],
82    b: &[T],
83) -> Result<(), SimdError>
84where
85    T: Scalar,
86    A: SimdArch + SimdKernel<T>,
87{
88    if a.len() != b.len() || (a.len() & 1) != 0 {
89        return Err(SimdError::LengthMismatch);
90    }
91
92    if A::REGISTER_WIDTH_BITS == 0 && a.len() >= 32_768 {
93        let mut lane = 0usize;
94        while lane + 8 <= a.len() {
95            let (re0, im0) = mul_pair::<T, CONJ_B>(a[lane], a[lane + 1], b[lane], b[lane + 1]);
96            a[lane] = re0;
97            a[lane + 1] = im0;
98
99            let (re1, im1) =
100                mul_pair::<T, CONJ_B>(a[lane + 2], a[lane + 3], b[lane + 2], b[lane + 3]);
101            a[lane + 2] = re1;
102            a[lane + 3] = im1;
103
104            let (re2, im2) =
105                mul_pair::<T, CONJ_B>(a[lane + 4], a[lane + 5], b[lane + 4], b[lane + 5]);
106            a[lane + 4] = re2;
107            a[lane + 5] = im2;
108
109            let (re3, im3) =
110                mul_pair::<T, CONJ_B>(a[lane + 6], a[lane + 7], b[lane + 6], b[lane + 7]);
111            a[lane + 6] = re3;
112            a[lane + 7] = im3;
113
114            lane += 8;
115        }
116        while lane < a.len() {
117            let (re, im) = mul_pair::<T, CONJ_B>(a[lane], a[lane + 1], b[lane], b[lane + 1]);
118            a[lane] = re;
119            a[lane + 1] = im;
120            lane += 2;
121        }
122        return Ok(());
123    }
124
125    let lanes = A::LANE_COUNT;
126    let mut offset = 0usize;
127
128    // The vectorized path requires an even lane count so every register holds
129    // whole (re, im) pairs; all provided backends satisfy this.
130    if lanes >= 2 && lanes & 1 == 0 {
131        while offset + 2 * lanes <= a.len() {
132            // SAFETY: offset + 2*lanes <= len was checked above; `a` and `b`
133            // are valid for reads (and `a` for writes) of `2*lanes`
134            // primitive values. `A`'s target features are guaranteed by the
135            // dispatching caller.
136            unsafe {
137                let av0 = A::load_unaligned(a.as_ptr().add(offset));
138                let bv0 = A::load_unaligned(b.as_ptr().add(offset));
139                let res0 = complex_mul_vector::<T, A, CONJ_B>(av0, bv0);
140                A::store_unaligned(a.as_mut_ptr().add(offset), res0);
141
142                let next = offset + lanes;
143                let av1 = A::load_unaligned(a.as_ptr().add(next));
144                let bv1 = A::load_unaligned(b.as_ptr().add(next));
145                let res1 = complex_mul_vector::<T, A, CONJ_B>(av1, bv1);
146                A::store_unaligned(a.as_mut_ptr().add(next), res1);
147            }
148            offset += 2 * lanes;
149        }
150
151        while offset + lanes <= a.len() {
152            // SAFETY: offset + lanes <= len was checked above; `a` and `b` are
153            // valid for reads (and `a` for writes) of `lanes` primitive values.
154            // `A`'s target features are guaranteed by the dispatching caller.
155            unsafe {
156                let av = A::load_unaligned(a.as_ptr().add(offset));
157                let bv = A::load_unaligned(b.as_ptr().add(offset));
158                let res = complex_mul_vector::<T, A, CONJ_B>(av, bv);
159                A::store_unaligned(a.as_mut_ptr().add(offset), res);
160            }
161            offset += lanes;
162        }
163    }
164
165    let mut lane = offset;
166    while lane < a.len() {
167        let (re, im) = mul_pair::<T, CONJ_B>(a[lane], a[lane + 1], b[lane], b[lane + 1]);
168        a[lane] = re;
169        a[lane + 1] = im;
170        lane += 2;
171    }
172
173    Ok(())
174}
175
176/// Computes an interleaved complex dot product using architecture `A`.
177///
178/// Inputs must have identical even lengths in `[re0, im0, re1, im1, ...]`
179/// primitive lane order. The returned tuple is `(re, im)` for
180/// `sum(a[k] * b[k])`; when `CONJ_B` is true, the operation is
181/// `sum(a[k] * conj(b[k]))`.
182///
183/// # Examples
184///
185/// ```
186/// use hermes_simd::{interleaved_complex_dot, Scalar};
187///
188/// let lhs = [1.0_f64, 2.0, 3.0, 4.0];
189/// let rhs = [5.0_f64, 6.0, 7.0, 8.0];
190///
191/// let product_sum = interleaved_complex_dot::<f64, Scalar, false>(&lhs, &rhs).unwrap();
192///
193/// assert_eq!(product_sum, (-18.0, 68.0));
194/// ```
195#[inline]
196pub fn interleaved_complex_dot<T, A, const CONJ_B: bool>(
197    a: &[T],
198    b: &[T],
199) -> Result<(T, T), SimdError>
200where
201    T: Scalar,
202    A: SimdArch + SimdKernel<T>,
203{
204    if a.len() != b.len() || (a.len() & 1) != 0 {
205        return Err(SimdError::LengthMismatch);
206    }
207
208    let lanes = A::LANE_COUNT;
209    let mut offset = 0usize;
210    let mut re = T::ZERO;
211    let mut im = T::ZERO;
212
213    if lanes >= 2 && lanes & 1 == 0 && offset + lanes <= a.len() {
214        assert!(
215            lanes <= MAX_STACK_LANES,
216            "SIMD lane count exceeds stack buffer"
217        );
218        // Two independent accumulators break the loop-carried add dependency
219        // so consecutive FMAs can overlap in the pipeline.
220        // SAFETY: feature availability is guaranteed by the dispatching caller.
221        let (mut acc0, mut acc1) = unsafe { (A::zero(), A::zero()) };
222        while offset + 2 * lanes <= a.len() {
223            // SAFETY: offset + 2*lanes <= len was checked above; `a` and `b`
224            // are valid for reads of `2*lanes` primitive values.
225            unsafe {
226                let av0 = A::load_unaligned(a.as_ptr().add(offset));
227                let bv0 = A::load_unaligned(b.as_ptr().add(offset));
228                acc0 = A::add(acc0, complex_mul_vector::<T, A, CONJ_B>(av0, bv0));
229                let av1 = A::load_unaligned(a.as_ptr().add(offset + lanes));
230                let bv1 = A::load_unaligned(b.as_ptr().add(offset + lanes));
231                acc1 = A::add(acc1, complex_mul_vector::<T, A, CONJ_B>(av1, bv1));
232            }
233            offset += 2 * lanes;
234        }
235        while offset + lanes <= a.len() {
236            // SAFETY: offset + lanes <= len was checked above; `a` and `b` are
237            // valid for reads of `lanes` primitive values.
238            unsafe {
239                let av = A::load_unaligned(a.as_ptr().add(offset));
240                let bv = A::load_unaligned(b.as_ptr().add(offset));
241                acc0 = A::add(acc0, complex_mul_vector::<T, A, CONJ_B>(av, bv));
242            }
243            offset += lanes;
244        }
245        // SAFETY: same feature contract as above.
246        let acc = unsafe { A::add(acc0, acc1) };
247
248        // Single spill: fold even lanes into re, odd lanes into im.
249        let mut buf = [T::ZERO; MAX_STACK_LANES];
250        // SAFETY: buf holds at least `lanes` elements (asserted above).
251        unsafe { A::store_unaligned(buf.as_mut_ptr(), acc) };
252        let mut lane = 0usize;
253        while lane < lanes {
254            re = re + buf[lane];
255            im = im + buf[lane + 1];
256            lane += 2;
257        }
258    }
259
260    let mut lane = offset;
261    while lane < a.len() {
262        let (prod_re, prod_im) = mul_pair::<T, CONJ_B>(a[lane], a[lane + 1], b[lane], b[lane + 1]);
263        re = re + prod_re;
264        im = im + prod_im;
265        lane += 2;
266    }
267
268    Ok((re, im))
269}
270
271#[runtime_dispatch(avx512f, avx2, neon, scalar)]
272pub(super) fn dispatch_interleaved_complex_mul_assign_impl<T, const CONJ_B: bool, A>(
273    a: &mut [T],
274    b: &[T],
275) -> Result<(), SimdError>
276where
277    T: Scalar,
278    A: SimdArch + SimdKernel<T>,
279{
280    interleaved_complex_mul_assign::<T, A, CONJ_B>(a, b)
281}
282
283#[runtime_dispatch(avx512f, avx2, neon, scalar)]
284pub(super) fn dispatch_interleaved_complex_dot_impl<T, const CONJ_B: bool, A>(
285    a: &[T],
286    b: &[T],
287) -> Result<(T, T), SimdError>
288where
289    T: Scalar,
290    A: SimdArch + SimdKernel<T>,
291{
292    interleaved_complex_dot::<T, A, CONJ_B>(a, b)
293}