Transmute struct into array in Rust
Let's say we have a structure, all fields of which are of the same sized types:
struct Homogeneous {
a: u64,
b: u64,
c: u64,
d: u64
}
And we have a "safe" way to construct it from array of bytes:
impl From<[u8; 32]> for Homogeneous {
fn from(slice: [u8; 32]) -> Self {
// helper macro to convert slice of u8s into u64
macro_rules! to_u64 {
($slice: expr, $at: expr) => {{
let ss = &$slice[$at..$at + 8];
let mut buf = [0u8; 8];
buf.copy_from_slice(&ss);
u64::from_ne_bytes(buf)
}};
}
Self {
a: to_u64!(bytes, 0),
b: to_u64!(bytes, 8),
c: to_u64!(bytes, 16),
d: to_u64!(bytes, 24),
}
}
}
Which is all good and it works. The question is whether unsafe solution (using transmute) is more efficient (safe?), also whether the reverse conversion will not cause UB due to optimizing compiler reordering struct fields?
impl From<[u8; 32]> for Homogeneous {
fn from(slice: [u8; 32]) -> Self {
unsafe { std::mem::transmute(slice) };
}
}
impl From<Homogeneous> for [u8; 32] {
fn from(h: Homogeneous) -> Self {
unsafe { std::mem::transmute(h) }
}
}
Those conversions work on my x86 processor using rust 1.57 compiler, and I wonder if they will always work, despite the architecture/compiler.
Solution 1:
From the rustlang reference:
The memory layout of a struct is undefined by default to allow for compiler optimizations like field reordering, but it can be fixed with the repr attribute. In either case, fields may be given in any order in a corresponding struct expression; the resulting struct value will always have the same memory layout.
This means that is not guaranteed that the attributes will be arranged as you wish. So you have to ensure it in your implementation so it will always work.
For example using #[repr(c)]
:
#[repr(c)]
struct Homogeneous {
a: u64,
b: u64,
c: u64,
d: u64
}
Solution 2:
Netwave already answered the part of the question about safety.
For the "more efficient" part, godbolt to the rescue:
Your code yields
<example::Homogeneous as core::convert::From<[u8; 32]>>::from:
mov rax, rdi
movups xmm0, xmmword ptr [rsi]
movups xmm1, xmmword ptr [rsi + 16]
movups xmmword ptr [rdi], xmm0
movups xmmword ptr [rdi + 16], xmm1
ret
and
#[repr(C)]
pub struct HomogeneousC { a: u64, b: u64, c: u64, d: u64 }
impl From<[u8; 32]> for HomogeneousC {
fn from(bytes: [u8; 32]) -> Self {
unsafe { std::mem::transmute(bytes) }
}
}
yields
<example::HomogeneousC as core::convert::From<[u8; 32]>>::from:
mov rax, rdi
movups xmm0, xmmword ptr [rsi]
movups xmm1, xmmword ptr [rsi + 16]
movups xmmword ptr [rdi + 16], xmm1
movups xmmword ptr [rdi], xmm0
ret
So, LLVM nicely optimizes away all the fluff of the safe version here, they'll probably have about equal performance.