arduano / simdeez

easy simd
MIT License
332 stars 25 forks source link

A library that abstracts over SIMD instruction sets, including ones with differing widths. SIMDeez is designed to allow you to write a function one time and produce SSE2, SSE41, and AVX2 versions of the function. You can either have the version you want chosen at compile time or automatically at runtime.

Originally developed by @jackmott, however I volunteered to take over ownership.

If there are intrinsics you need that are not currently implemented, create an issue and I'll add them. PRs to add more intrinsics are welcome. Currently things are well fleshed out for i32, i64, f32, and f64 types.

As Rust stabilizes support for Neon and AVX-512 I plan to add those as well.

Refer to the excellent Intel Intrinsics Guide for documentation on these functions:

Features

Trig Functions via Sleef-sys

A number of trigonometric and other common math functions are provided in vectorized form via the Sleef-sys crate. This is an optional feature sleef that you can enable. Doing so currently requires nightly, as well as having CMake and Clang installed.

Compared to packed_simd

Compared to Faster

All of the above could change! Faster seems to generally have the same performance as long as you don't run into some of the slower fallback functions.

Example

use simdeez::*;
    use simdeez::scalar::*;
    use simdeez::sse2::*;
    use simdeez::sse41::*;
    use simdeez::avx::*;
    use simdeez::avx2::*;
    // If you want your SIMD function to use use runtime feature detection to call
    // the fastest available version, use the simd_runtime_generate macro:
    simd_runtime_generate!(
    fn distance(
        x1: &[f32],
        y1: &[f32],
        x2: &[f32],
        y2: &[f32]) -> Vec<f32> {

        let mut result: Vec<f32> = Vec::with_capacity(x1.len());
        result.set_len(x1.len()); // for efficiency

        /// Set each slice to the same length for iteration efficiency
        let mut x1 = &x1[..x1.len()];
        let mut y1 = &y1[..x1.len()];
        let mut x2 = &x2[..x1.len()];
        let mut y2 = &y2[..x1.len()];
        let mut res = &mut result[..x1.len()];

        // Operations have to be done in terms of the vector width
        // so that it will work with any size vector.
        // the width of a vector type is provided as a constant
        // so the compiler is free to optimize it more.
        // S::VF32_WIDTH is a constant, 4 when using SSE, 8 when using AVX2, etc
        while x1.len() >= S::VF32_WIDTH {
            //load data from your vec into an SIMD value
            let xv1 = S::loadu_ps(&x1[0]);
            let yv1 = S::loadu_ps(&y1[0]);
            let xv2 = S::loadu_ps(&x2[0]);
            let yv2 = S::loadu_ps(&y2[0]);

            // Use the usual intrinsic syntax if you prefer
            let mut xdiff = S::sub_ps(xv1, xv2);
            // Or use operater overloading if you like
            let mut ydiff = yv1 - yv2;
            xdiff *= xdiff;
            ydiff *= ydiff;
            let distance = S::sqrt_ps(xdiff + ydiff);
            // Store the SIMD value into the result vec
            S::storeu_ps(&mut res[0], distance);

            // Move each slice to the next position
            x1 = &x1[S::VF32_WIDTH..];
            y1 = &y1[S::VF32_WIDTH..];
            x2 = &x2[S::VF32_WIDTH..];
            y2 = &y2[S::VF32_WIDTH..];
            res = &mut res[S::VF32_WIDTH..];
        }

        // (Optional) Compute the remaining elements. Not necessary if you are sure the length
        // of your data is always a multiple of the maximum S::VF32_WIDTH you compile for (4 for SSE, 8 for AVX2, etc).
        // This can be asserted by putting `assert_eq!(x1.len(), 0);` here
        for i in 0..x1.len() {
            let mut xdiff = x1[i] - x2[i];
            let mut ydiff = y1[i] - y2[i];
            xdiff *= xdiff;
            ydiff *= ydiff;
            let distance = (xdiff + ydiff).sqrt();
            res[i] = distance;
        }

        result
    });
fn main() {
}

This will generate 5 functions for you:

You can use any of these you wish, though typically you would use the runtime_select version unless you want to force an older instruction set to avoid throttling or for other arcane reasons. Optionally you can use the simd_compiletime_generate! macro in the same way. This will produce 2 active functions via the cfg attribute feature:

You may also forgo the macros if you know what you are doing, just keep in mind there are lots of arcane subtleties with inlining and target_features that must be managed. See how the macros expand for more detail.