Skip to main content

argminmax/simd/
simd_u64.rs

1/// Implementation of the argminmax operations for u64.
2/// As there are no SIMD instructions for uints (on x86 & x86_64) we transform the u64
3/// values to i64 ordinal values:
4///     ord_i64 = v ^ -0x8000000000000000
5///
6/// This transformation is a bijection, i.e. it is reversible:
7///     v = ord_i64 ^ -0x8000000000000000
8///
9/// Through this transformation we can perform the argminmax operations using SIMD on
10/// the ordinal integer values and then transform the result back to the original u64
11/// values.
12///
13
14#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
15use super::config::SIMDInstructionSet;
16#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
17use super::generic::{impl_SIMDArgMinMax, impl_SIMDInit_Int};
18#[cfg(any(
19    target_arch = "x86",
20    target_arch = "x86_64",
21    all(target_arch = "arm", feature = "nightly_simd"),
22    target_arch = "aarch64",
23))]
24use super::generic::{SIMDArgMinMax, SIMDInit, SIMDOps};
25#[cfg(any(
26    target_arch = "x86",
27    target_arch = "x86_64",
28    all(target_arch = "arm", feature = "nightly_simd"),
29    target_arch = "aarch64",
30))]
31use crate::SCALAR;
32#[cfg(target_arch = "aarch64")]
33use std::arch::aarch64::*;
34#[cfg(target_arch = "x86")]
35use std::arch::x86::*;
36#[cfg(target_arch = "x86_64")]
37use std::arch::x86_64::*;
38
39/// The dtype-strategy for performing operations on u64 data: (default) Int
40#[cfg(any(
41    target_arch = "x86",
42    target_arch = "x86_64",
43    all(target_arch = "arm", feature = "nightly_simd"),
44    target_arch = "aarch64",
45))]
46use super::super::dtype_strategy::Int;
47
48#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
49use super::task::{max_index_value, min_index_value};
50
51#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
52const XOR_VALUE: i64 = -0x8000000000000000; // i64::MIN
53
54#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
55#[inline(always)]
56#[allow(unnecessary_transmutes)]
57fn _i64ord_to_u64(ord_i64: i64) -> u64 {
58    // let v = ord_i64 ^ -0x8000000000000000;
59    unsafe { std::mem::transmute::<i64, u64>(ord_i64 ^ XOR_VALUE) }
60}
61
62#[cfg(any(target_arch = "x86", target_arch = "x86_64",))]
63const MAX_INDEX: usize = i64::MAX as usize; // SIMD operations on signed ints
64#[cfg(target_arch = "aarch64")]
65const MAX_INDEX: usize = u64::MAX as usize; // SIMD operations on unsigned ints
66
67// --------------------------------------- AVX2 ----------------------------------------
68
69#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
70mod avx2 {
71    use super::super::config::AVX2;
72    use super::*;
73
74    const LANE_SIZE: usize = AVX2::<Int>::LANE_SIZE_64;
75    const XOR_MASK: __m256i = unsafe { std::mem::transmute([XOR_VALUE; LANE_SIZE]) };
76
77    #[inline(always)]
78    unsafe fn _u64_as_m256i_to_i64ord(u64_as_m256i: __m256i) -> __m256i {
79        // on a scalar: v ^ -0x8000000000000000
80        // transforms to monotonically increasing order
81        _mm256_xor_si256(u64_as_m256i, XOR_MASK)
82    }
83
84    #[inline(always)]
85    unsafe fn _reg_to_i64_arr(reg: __m256i) -> [i64; LANE_SIZE] {
86        std::mem::transmute::<__m256i, [i64; LANE_SIZE]>(reg)
87    }
88
89    impl SIMDOps<u64, __m256i, __m256i, LANE_SIZE> for AVX2<Int> {
90        const INITIAL_INDEX: __m256i = unsafe { std::mem::transmute([0i64, 1i64, 2i64, 3i64]) };
91        const INDEX_INCREMENT: __m256i =
92            unsafe { std::mem::transmute([LANE_SIZE as i64; LANE_SIZE]) };
93        const MAX_INDEX: usize = MAX_INDEX;
94
95        #[inline(always)]
96        unsafe fn _reg_to_arr(_: __m256i) -> [u64; LANE_SIZE] {
97            // Not implemented because we will perform the horizontal operations on the
98            // signed integer values instead of trying to retransform **only** the values
99            // (and thus not the indices) to signed integers.
100            unimplemented!()
101        }
102
103        #[inline(always)]
104        unsafe fn _mm_loadu(data: *const u64) -> __m256i {
105            _u64_as_m256i_to_i64ord(_mm256_loadu_si256(data as *const __m256i))
106        }
107
108        #[inline(always)]
109        unsafe fn _mm_add(a: __m256i, b: __m256i) -> __m256i {
110            _mm256_add_epi64(a, b)
111        }
112
113        #[inline(always)]
114        unsafe fn _mm_cmpgt(a: __m256i, b: __m256i) -> __m256i {
115            _mm256_cmpgt_epi64(a, b)
116        }
117
118        #[inline(always)]
119        unsafe fn _mm_cmplt(a: __m256i, b: __m256i) -> __m256i {
120            _mm256_cmpgt_epi64(b, a)
121        }
122
123        #[inline(always)]
124        unsafe fn _mm_blendv(a: __m256i, b: __m256i, mask: __m256i) -> __m256i {
125            _mm256_blendv_epi8(a, b, mask)
126        }
127
128        #[inline(always)]
129        unsafe fn _horiz_min(index: __m256i, value: __m256i) -> (usize, u64) {
130            let index_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(index);
131            let value_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(value);
132            let (min_index, min_value) = min_index_value(&index_arr, &value_arr);
133            (min_index as usize, _i64ord_to_u64(min_value))
134        }
135
136        #[inline(always)]
137        unsafe fn _horiz_max(index: __m256i, value: __m256i) -> (usize, u64) {
138            let index_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(index);
139            let value_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(value);
140            let (max_index, max_value) = max_index_value(&index_arr, &value_arr);
141            (max_index as usize, _i64ord_to_u64(max_value))
142        }
143    }
144
145    impl_SIMDInit_Int!(u64, __m256i, __m256i, LANE_SIZE, AVX2<Int>);
146
147    impl_SIMDArgMinMax!(
148        u64,
149        __m256i,
150        __m256i,
151        LANE_SIZE,
152        SCALAR<Int>,
153        AVX2<Int>,
154        "avx2"
155    );
156}
157
158// ---------------------------------------- SSE ----------------------------------------
159
160#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
161mod sse {
162    use super::super::config::SSE;
163    use super::*;
164
165    const LANE_SIZE: usize = SSE::<Int>::LANE_SIZE_64;
166    const XOR_MASK: __m128i = unsafe { std::mem::transmute([XOR_VALUE; LANE_SIZE]) };
167
168    #[inline(always)]
169    unsafe fn _u64_as_m128i_to_i64ord(u64_as_m128i: __m128i) -> __m128i {
170        // on a scalar: v ^ -0x8000000000000000
171        // transforms to monotonically increasing order
172        _mm_xor_si128(u64_as_m128i, XOR_MASK)
173    }
174
175    #[inline(always)]
176    unsafe fn _reg_to_i64_arr(reg: __m128i) -> [i64; LANE_SIZE] {
177        std::mem::transmute::<__m128i, [i64; LANE_SIZE]>(reg)
178    }
179
180    impl SIMDOps<u64, __m128i, __m128i, LANE_SIZE> for SSE<Int> {
181        const INITIAL_INDEX: __m128i = unsafe { std::mem::transmute([0i64, 1i64]) };
182        const INDEX_INCREMENT: __m128i =
183            unsafe { std::mem::transmute([LANE_SIZE as i64; LANE_SIZE]) };
184        const MAX_INDEX: usize = MAX_INDEX;
185
186        #[inline(always)]
187        unsafe fn _reg_to_arr(_: __m128i) -> [u64; LANE_SIZE] {
188            // Not implemented because we will perform the horizontal operations on the
189            // signed integer values instead of trying to retransform **only** the values
190            // (and thus not the indices) to signed integers.
191            unimplemented!()
192        }
193
194        #[inline(always)]
195        unsafe fn _mm_loadu(data: *const u64) -> __m128i {
196            _u64_as_m128i_to_i64ord(_mm_loadu_si128(data as *const __m128i))
197        }
198
199        #[inline(always)]
200        unsafe fn _mm_add(a: __m128i, b: __m128i) -> __m128i {
201            _mm_add_epi64(a, b)
202        }
203
204        #[inline(always)]
205        unsafe fn _mm_cmpgt(a: __m128i, b: __m128i) -> __m128i {
206            _mm_cmpgt_epi64(a, b)
207        }
208
209        #[inline(always)]
210        unsafe fn _mm_cmplt(a: __m128i, b: __m128i) -> __m128i {
211            _mm_cmpgt_epi64(b, a)
212        }
213
214        #[inline(always)]
215        unsafe fn _mm_blendv(a: __m128i, b: __m128i, mask: __m128i) -> __m128i {
216            _mm_blendv_epi8(a, b, mask)
217        }
218
219        #[inline(always)]
220        unsafe fn _horiz_min(index: __m128i, value: __m128i) -> (usize, u64) {
221            let index_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(index);
222            let value_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(value);
223            let (min_index, min_value) = min_index_value(&index_arr, &value_arr);
224            (min_index as usize, _i64ord_to_u64(min_value))
225        }
226
227        #[inline(always)]
228        unsafe fn _horiz_max(index: __m128i, value: __m128i) -> (usize, u64) {
229            let index_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(index);
230            let value_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(value);
231            let (max_index, max_value) = max_index_value(&index_arr, &value_arr);
232            (max_index as usize, _i64ord_to_u64(max_value))
233        }
234    }
235
236    impl_SIMDInit_Int!(u64, __m128i, __m128i, LANE_SIZE, SSE<Int>);
237
238    impl_SIMDArgMinMax!(
239        u64,
240        __m128i,
241        __m128i,
242        LANE_SIZE,
243        SCALAR<Int>,
244        SSE<Int>,
245        "sse4.2"
246    );
247}
248
249// -------------------------------------- AVX512 ---------------------------------------
250
251#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
252#[cfg(feature = "nightly_simd")]
253mod avx512 {
254    use super::super::config::AVX512;
255    use super::*;
256
257    const LANE_SIZE: usize = AVX512::<Int>::LANE_SIZE_64;
258    const XOR_MASK: __m512i = unsafe { std::mem::transmute([XOR_VALUE; LANE_SIZE]) };
259
260    #[inline(always)]
261    unsafe fn _u64_as_m512i_to_i64ord(u64_as_m512i: __m512i) -> __m512i {
262        // on a scalar: v ^ -0x8000000000000000
263        // transforms to monotonically increasing order
264        _mm512_xor_si512(u64_as_m512i, XOR_MASK)
265    }
266
267    #[inline(always)]
268    unsafe fn _reg_to_i64_arr(reg: __m512i) -> [i64; LANE_SIZE] {
269        std::mem::transmute::<__m512i, [i64; LANE_SIZE]>(reg)
270    }
271
272    impl SIMDOps<u64, __m512i, u8, LANE_SIZE> for AVX512<Int> {
273        const INITIAL_INDEX: __m512i =
274            unsafe { std::mem::transmute([0i64, 1i64, 2i64, 3i64, 4i64, 5i64, 6i64, 7i64]) };
275        const INDEX_INCREMENT: __m512i =
276            unsafe { std::mem::transmute([LANE_SIZE as i64; LANE_SIZE]) };
277        const MAX_INDEX: usize = MAX_INDEX;
278
279        #[inline(always)]
280        unsafe fn _reg_to_arr(_: __m512i) -> [u64; LANE_SIZE] {
281            // Not implemented because we will perform the horizontal operations on the
282            // signed integer values instead of trying to retransform **only** the values
283            // (and thus not the indices) to signed integers.
284            unimplemented!()
285        }
286
287        #[inline(always)]
288        unsafe fn _mm_loadu(data: *const u64) -> __m512i {
289            _u64_as_m512i_to_i64ord(_mm512_loadu_epi64(data as *const i64))
290        }
291
292        #[inline(always)]
293        unsafe fn _mm_add(a: __m512i, b: __m512i) -> __m512i {
294            _mm512_add_epi64(a, b)
295        }
296
297        #[inline(always)]
298        unsafe fn _mm_cmpgt(a: __m512i, b: __m512i) -> u8 {
299            _mm512_cmpgt_epi64_mask(a, b)
300        }
301
302        #[inline(always)]
303        unsafe fn _mm_cmplt(a: __m512i, b: __m512i) -> u8 {
304            _mm512_cmplt_epi64_mask(a, b)
305        }
306
307        #[inline(always)]
308        unsafe fn _mm_blendv(a: __m512i, b: __m512i, mask: u8) -> __m512i {
309            _mm512_mask_blend_epi64(mask, a, b)
310        }
311
312        #[inline(always)]
313        unsafe fn _horiz_min(index: __m512i, value: __m512i) -> (usize, u64) {
314            let index_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(index);
315            let value_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(value);
316            let (min_index, min_value) = min_index_value(&index_arr, &value_arr);
317            (min_index as usize, _i64ord_to_u64(min_value))
318        }
319
320        #[inline(always)]
321        unsafe fn _horiz_max(index: __m512i, value: __m512i) -> (usize, u64) {
322            let index_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(index);
323            let value_arr: [i64; LANE_SIZE] = _reg_to_i64_arr(value);
324            let (max_index, max_value) = max_index_value(&index_arr, &value_arr);
325            (max_index as usize, _i64ord_to_u64(max_value))
326        }
327    }
328
329    impl_SIMDInit_Int!(u64, __m512i, u8, LANE_SIZE, AVX512<Int>);
330
331    impl_SIMDArgMinMax!(
332        u64,
333        __m512i,
334        u8,
335        LANE_SIZE,
336        SCALAR<Int>,
337        AVX512<Int>,
338        "avx512f"
339    );
340}
341
342// --------------------------------------- NEON ----------------------------------------
343
344// There are NEON SIMD intrinsics for u64, but
345//  - for arm we miss the vcgt_ and vclt_ intrinsics.
346//  - for aarch64 the required intrinsics are present (on nightly)
347
348#[cfg(target_arch = "arm")]
349#[cfg(feature = "nightly_simd")]
350mod neon {
351    use super::super::config::NEON;
352    use super::super::generic::{unimpl_SIMDArgMinMax, unimpl_SIMDInit, unimpl_SIMDOps};
353    use super::*;
354
355    // We need to (un)implement the SIMD trait for the NEON struct as otherwise the
356    // compiler will complain that the trait is not implemented for the struct -
357    // even though we are not using the trait for the NEON struct when dealing with
358    // > 64 bit data types.
359    unimpl_SIMDOps!(u64, usize, NEON<Int>);
360    unimpl_SIMDInit!(u64, usize, NEON<Int>);
361    unimpl_SIMDArgMinMax!(u64, usize, SCALAR<Int>, NEON<Int>);
362}
363
364#[cfg(target_arch = "aarch64")] // stable for AArch64
365mod neon {
366    use super::super::config::NEON;
367    use super::*;
368
369    const LANE_SIZE: usize = NEON::<Int>::LANE_SIZE_64;
370
371    impl SIMDOps<u64, uint64x2_t, uint64x2_t, LANE_SIZE> for NEON<Int> {
372        const INITIAL_INDEX: uint64x2_t = unsafe { std::mem::transmute([0u64, 1u64]) };
373        const INDEX_INCREMENT: uint64x2_t =
374            unsafe { std::mem::transmute([LANE_SIZE as i64; LANE_SIZE]) };
375        const MAX_INDEX: usize = MAX_INDEX;
376
377        #[inline(always)]
378        unsafe fn _reg_to_arr(reg: uint64x2_t) -> [u64; LANE_SIZE] {
379            std::mem::transmute::<uint64x2_t, [u64; LANE_SIZE]>(reg)
380        }
381
382        #[inline(always)]
383        unsafe fn _mm_loadu(data: *const u64) -> uint64x2_t {
384            vld1q_u64(data)
385        }
386
387        #[inline(always)]
388        unsafe fn _mm_add(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t {
389            vaddq_u64(a, b)
390        }
391
392        #[inline(always)]
393        unsafe fn _mm_cmpgt(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t {
394            vcgtq_u64(a, b)
395        }
396
397        #[inline(always)]
398        unsafe fn _mm_cmplt(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t {
399            vcltq_u64(a, b)
400        }
401
402        #[inline(always)]
403        unsafe fn _mm_blendv(a: uint64x2_t, b: uint64x2_t, mask: uint64x2_t) -> uint64x2_t {
404            vbslq_u64(mask, b, a)
405        }
406    }
407
408    impl_SIMDInit_Int!(u64, uint64x2_t, uint64x2_t, LANE_SIZE, NEON<Int>);
409
410    impl_SIMDArgMinMax!(
411        u64,
412        uint64x2_t,
413        uint64x2_t,
414        LANE_SIZE,
415        SCALAR<Int>,
416        NEON<Int>,
417        "neon"
418    );
419}
420
421// ======================================= TESTS =======================================
422
423#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
424#[cfg(test)]
425mod tests {
426    use rstest::rstest;
427    use rstest_reuse::{self, *};
428    use std::marker::PhantomData;
429
430    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
431    #[cfg(feature = "nightly_simd")]
432    use crate::simd::config::AVX512;
433    #[cfg(target_arch = "aarch64")]
434    use crate::simd::config::NEON;
435    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
436    use crate::simd::config::{AVX2, SSE};
437    use crate::{Int, SIMDArgMinMax, SCALAR};
438
439    use super::super::test_utils::{
440        test_first_index_identical_values_argminmax, test_return_same_result_argminmax,
441    };
442
443    use dev_utils::utils;
444
445    fn get_array_u64(n: usize) -> Vec<u64> {
446        utils::SampleUniformFullRange::get_random_array(n)
447    }
448
449    // The scalar implementation
450    const SCALAR_STRATEGY: SCALAR<Int> = SCALAR {
451        _dtype_strategy: PhantomData::<Int>,
452    };
453
454    // ------------ Template for x86 / x86_64 -------------
455
456    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
457    #[template]
458    #[rstest]
459    #[case::sse(SSE {_dtype_strategy: PhantomData::<Int>}, is_x86_feature_detected!("sse4.2"))]
460    #[case::avx2(AVX2 {_dtype_strategy: PhantomData::<Int>}, is_x86_feature_detected!("avx2"))]
461    #[cfg_attr(feature = "nightly_simd", case::avx512(AVX512 {_dtype_strategy: PhantomData::<Int>}, is_x86_feature_detected!("avx512f")))]
462    fn simd_implementations<T, SIMDV, SIMDM, const LANE_SIZE: usize>(
463        #[case] simd: T,
464        #[case] simd_available: bool,
465    ) {
466    }
467
468    // --------------- Template for AArch64 ---------------
469
470    #[cfg(target_arch = "aarch64")]
471    #[template]
472    #[rstest]
473    #[case::neon(NEON {_dtype_strategy: PhantomData::<Int>}, true)]
474    fn simd_implementations<T, SIMDV, SIMDM, const LANE_SIZE: usize>(
475        #[case] simd: T,
476        #[case] simd_available: bool,
477    ) {
478    }
479
480    // ----------------- The actual tests -----------------
481
482    #[apply(simd_implementations)]
483    fn test_first_index_is_returned_when_identical_values_found<
484        T,
485        SIMDV,
486        SIMDM,
487        const LANE_SIZE: usize,
488    >(
489        #[case] simd: T,
490        #[case] simd_available: bool,
491    ) where
492        T: SIMDArgMinMax<u64, SIMDV, SIMDM, LANE_SIZE, SCALAR<Int>>,
493        SIMDV: Copy,
494        SIMDM: Copy,
495    {
496        if !simd_available {
497            return;
498        }
499        test_first_index_identical_values_argminmax(SCALAR_STRATEGY, simd);
500    }
501
502    #[apply(simd_implementations)]
503    fn test_return_same_result<T, SIMDV, SIMDM, const LANE_SIZE: usize>(
504        #[case] simd: T,
505        #[case] simd_available: bool,
506    ) where
507        T: SIMDArgMinMax<u64, SIMDV, SIMDM, LANE_SIZE, SCALAR<Int>>,
508        SIMDV: Copy,
509        SIMDM: Copy,
510    {
511        if !simd_available {
512            return;
513        }
514        test_return_same_result_argminmax(get_array_u64, SCALAR_STRATEGY, simd);
515    }
516}