Search code examples
arraysrustsimdreinterpret-cast

Is this transmute from array of SIMD to regular numeric types undefined behaviour?


I have an array of floating point values and I need to do some easily vectorizable operations like taking the sum of all of them and then dividing them all by that sum. I also need to access (mostly read) individual elements of the array. I figured I could use a SIMD type to enable the vectorization of the array. Whenever I'd need to do a lot of things with individual elements of the array, I would transmute the array into a reference to an array of regular floating point values, and access that reference instead, like so:

extern crate simd;

use simd::x86::avx::f32x8;

fn main() {
    let values8: [f32x8; 100] = [f32x8::splat(1.1); 100];
    let values: &[f32; 800] = unsafe { std::mem::transmute(&values8) };

    println!("{}", values[333]);
}

This compiles and seems to work just fine. But I'm worried that this is undefined behaviour because I read that:

Transmuting between non-repr(C) types is UB

I think SIMD types (like simd::x86::avx::f32x8) are repr(simd) and I don't think [f32; 800] is repr(C) either.

I know I can use the extract method on SIMD types to get those individual floating point values, but using the aforementioned transmute-to-a-regular-array scheme would make the code much simpler.


Solution

  • The early definition in this Pre-RFC states that

    It is illegal to take an internal reference to the fields of a repr(simd) type, because the representation of booleans may require to change, so that booleans are bit-packed. The official external library providing SIMD support will have private fields so this will not be generally observable.

    This would obviously forbid turning a simd type into an array.

    The actual RFC changed that, so apparently you are allowed to reference the inner things. The RFC also states that the layout and alignments are platform dependent.

    Since all platforms that I know of don't add padding to trivial simd types like f32x8, you can assume that the layout of a f32x8 is "the same" as a [f32; 8] in terms of that it contains 8 f32s tightly packed in a 32 byte piece of memory. The order may be arbitrary though.


    "Transmuting between non-repr(C) types is UB" I think simd types (like simd::x86::avx::f32x8) are repr(simd) and I don't think [f32; 800] is repr(C) either.

    Technically you are neither transmuting a repr(simd) type nor a [f32; 800], you are transmuting a reference to another reference, but the result is the same.


    As noted by @Chris Emerson, the unsafety in your example comes from the breaking of the lifetime chain. To reinstate that, create an abstraction boundary:

    fn simd_slice(simd: &[f32x8; 100]) -> &[f32; 800] {
        unsafe { &*(simd as *const [f32x8; 100] as *const [f32; 800]) }
    }