Skip to main content

rten_simd/
dispatch.rs

1use std::mem::MaybeUninit;
2
3use crate::Isa;
4use crate::functional::simd_map;
5use crate::ops::{GetNumOps, GetSimd};
6use crate::span::SrcDest;
7
8/// A vectorized operation which can be instantiated for different instruction
9/// sets.
10pub trait SimdOp {
11    /// The type of the operation's result.
12    type Output;
13
14    /// Evaluate the operation using the given instruction set.
15    fn eval<I: Isa>(self, isa: I) -> Self::Output;
16
17    /// Dispatch this operation using the preferred ISA for the current platform.
18    fn dispatch(self) -> Self::Output
19    where
20        Self: Sized,
21    {
22        dispatch(self)
23    }
24}
25
26/// Invoke a SIMD operation using the preferred ISA for the current system.
27///
28/// This function will check the available SIMD instruction sets and then
29/// dispatch to [`SimdOp::eval`], passing the selected [`Isa`].
30pub fn dispatch<Op: SimdOp>(op: Op) -> Op::Output {
31    #[cfg(target_arch = "aarch64")]
32    if let Some(isa) = super::arch::aarch64::ArmNeonIsa::new() {
33        return op.eval(isa);
34    }
35
36    #[cfg(target_arch = "x86_64")]
37    {
38        {
39            // The target features enabled here must match those tested for by `Avx512Isa::new`.
40            #[target_feature(enable = "avx512f")]
41            #[target_feature(enable = "avx512vl")]
42            #[target_feature(enable = "avx512bw")]
43            #[target_feature(enable = "avx512dq")]
44            #[target_feature(enable = "f16c")]
45            unsafe fn dispatch_avx512<Op: SimdOp>(isa: impl Isa, op: Op) -> Op::Output {
46                op.eval(isa)
47            }
48
49            if let Some(isa) = super::arch::x86_64::Avx512Isa::new() {
50                // Safety: AVX-512 is supported
51                unsafe {
52                    return dispatch_avx512(isa, op);
53                }
54            }
55        }
56
57        // The target features enabled here must match those tested for by `Avx2Isa::new`.
58        #[target_feature(enable = "avx2")]
59        #[target_feature(enable = "avx")]
60        #[target_feature(enable = "fma")]
61        #[target_feature(enable = "f16c")]
62        unsafe fn dispatch_avx2<Op: SimdOp>(isa: impl Isa, op: Op) -> Op::Output {
63            op.eval(isa)
64        }
65
66        if let Some(isa) = super::arch::x86_64::Avx2Isa::new() {
67            // Safety: AVX2 is supported
68            unsafe {
69                return dispatch_avx2(isa, op);
70            }
71        }
72    }
73
74    #[cfg(target_arch = "wasm32")]
75    #[cfg(target_feature = "simd128")]
76    {
77        if let Some(isa) = super::arch::wasm32::Wasm32Isa::new() {
78            return op.eval(isa);
79        }
80    }
81
82    let isa = super::arch::generic::GenericIsa::new();
83    op.eval(isa)
84}
85
86/// Convenience trait for defining vectorized unary operations.
87pub trait SimdUnaryOp<T: GetSimd> {
88    /// Evaluate the unary function on the elements in `x`.
89    ///
90    /// ```
91    /// use rten_simd::{Isa, Simd, SimdUnaryOp};
92    /// use rten_simd::ops::{FloatOps, NumOps};
93    ///
94    /// struct Reciprocal {}
95    ///
96    /// impl SimdUnaryOp<f32> for Reciprocal {
97    ///     fn eval<I: Isa>(&self, isa: I, x: I::F32) -> I::F32 {
98    ///         let ops = isa.f32();
99    ///         ops.div(ops.one(), x)
100    ///     }
101    /// }
102    /// ```
103    fn eval<I: Isa>(&self, isa: I, x: T::Simd<I>) -> T::Simd<I>;
104
105    /// Evaluate the unary function on elements in `x`.
106    ///
107    /// This is a shorthand for `Self::default().eval(x)`. It is mainly useful
108    /// when one vectorized operation needs to call another as part of its
109    /// implementation.
110    #[inline(always)]
111    fn apply<I: Isa>(isa: I, x: T::Simd<I>) -> T::Simd<I>
112    where
113        Self: Default,
114    {
115        Self::default().eval(isa, x)
116    }
117
118    /// Apply this function to a slice.
119    ///
120    /// This reads elements from `input` in SIMD vector-sized chunks, applies
121    /// the operation and writes the results to `output`.
122    fn map<'dst>(&self, input: &[T], output: &'dst mut [MaybeUninit<T>]) -> &'dst mut [T]
123    where
124        Self: Sized,
125        T: GetNumOps,
126    {
127        let wrapped_op = SimdMapOp::wrap((input, output).into(), self);
128        dispatch(wrapped_op)
129    }
130
131    /// Apply a vectorized unary function to a mutable slice.
132    ///
133    /// This is similar to [`map`](SimdUnaryOp::map) but reads and writes
134    /// to the same slice.
135    #[allow(private_bounds)]
136    fn map_mut(&self, input: &mut [T])
137    where
138        Self: Sized,
139        T: GetNumOps,
140    {
141        let wrapped_op = SimdMapOp::wrap(input.into(), self);
142        dispatch(wrapped_op);
143    }
144
145    /// Apply this operation to a single element.
146    fn scalar_eval(&self, x: T) -> T
147    where
148        Self: Sized,
149        T: GetNumOps,
150    {
151        let mut array = [x];
152        self.map_mut(&mut array);
153        array[0]
154    }
155}
156
157/// SIMD operation which applies a unary operator `Op` to all elements in
158/// an input buffer using [`simd_map`].
159struct SimdMapOp<'src, 'dst, 'op, T: GetSimd, Op: SimdUnaryOp<T>> {
160    src_dest: SrcDest<'src, 'dst, T>,
161    op: &'op Op,
162}
163
164impl<'src, 'dst, 'op, T: GetSimd, Op: SimdUnaryOp<T>> SimdMapOp<'src, 'dst, 'op, T, Op> {
165    pub fn wrap(src_dest: SrcDest<'src, 'dst, T>, op: &'op Op) -> Self {
166        SimdMapOp { src_dest, op }
167    }
168}
169
170impl<'dst, T: GetNumOps + GetSimd, Op: SimdUnaryOp<T>> SimdOp for SimdMapOp<'_, 'dst, '_, T, Op> {
171    type Output = &'dst mut [T];
172
173    #[inline(always)]
174    fn eval<I: Isa>(self, isa: I) -> Self::Output {
175        simd_map(
176            T::num_ops(isa),
177            self.src_dest,
178            #[inline(always)]
179            |x| self.op.eval(isa, x),
180        )
181    }
182}
183
184/// Convenience macro for defining and evaluating a SIMD operation.
185#[cfg(test)]
186macro_rules! test_simd_op {
187    ($isa:ident, $op:block) => {{
188        struct TestOp {}
189
190        impl SimdOp for TestOp {
191            type Output = ();
192
193            fn eval<I: Isa>(self, $isa: I) {
194                $op
195            }
196        }
197
198        TestOp {}.dispatch()
199    }};
200}
201
202#[cfg(test)]
203pub(crate) use test_simd_op;
204
205#[cfg(test)]
206mod tests {
207    use super::SimdUnaryOp;
208    use crate::Isa;
209    use crate::ops::{FloatOps, GetNumOps, GetSimd, NumOps};
210
211    #[test]
212    fn test_unary_float_op() {
213        struct Reciprocal {}
214
215        impl SimdUnaryOp<f32> for Reciprocal {
216            fn eval<I: Isa>(&self, isa: I, x: I::F32) -> I::F32 {
217                let ops = isa.f32();
218                ops.div(ops.one(), x)
219            }
220        }
221
222        let mut buf = [1., 2., 3., 4.];
223        Reciprocal {}.map_mut(&mut buf);
224
225        assert_eq!(buf, [1., 1. / 2., 1. / 3., 1. / 4.]);
226    }
227
228    #[test]
229    fn test_unary_generic_op() {
230        struct Double {}
231
232        impl<T> SimdUnaryOp<T> for Double
233        where
234            T: GetSimd + GetNumOps,
235        {
236            fn eval<I: Isa>(&self, isa: I, x: T::Simd<I>) -> T::Simd<I> {
237                let ops = T::num_ops(isa);
238                ops.add(x, x)
239            }
240        }
241
242        let mut buf = [1i32, 2, 3, 4];
243        Double {}.map_mut(&mut buf);
244        assert_eq!(buf, [2, 4, 6, 8]);
245
246        let mut buf = [1.0f32, 2., 3., 4.];
247        Double {}.map_mut(&mut buf);
248        assert_eq!(buf, [2., 4., 6., 8.]);
249    }
250}