When is transmute acceptable? What could be done instead?

I've always had a "Transmute is never the answer" policy for as long as I've been using Rust. But in this example benchmark, recreating the type is more than 100x slower.

When others have encountered this kind of case, what alternative approaches have worked?

Thank you for any thoughts.

bench          fastest       │ slowest       │ median        │ mean          │ samples │ iters
├─ fast_bench                │               │               │               │         │
│  ╰─ 10       6.903 µs      │ 95.51 µs      │ 8.773 µs      │ 9.255 µs      │ 5000    │ 5000
╰─ slow_bench                │               │               │               │         │
   ╰─ 10       964.4 µs      │ 4.591 ms      │ 1.106 ms      │ 1.172 ms      │ 5000    │ 5000
enum MaybeOwned<'a> {
    Borrowed(&'a [u8]),
    Owned(Vec<u8>)
}

impl<'a> MaybeOwned<'a> {
    fn ensure_owned(&mut self) {
        match self {
            Self::Owned(_) => {},
            Self::Borrowed(slice) => {
                *self = Self::Owned(slice.to_vec())
            }
        }
    }
}

struct BigStruct<'a> {
    maybe: MaybeOwned<'a>,
    lots_o_data: [u8; 1048576],
}

fn ensure_owned_fast(mut input: Box<BigStruct>) -> Box<BigStruct<'static>> {
    input.maybe.ensure_owned();
    unsafe{ core::mem::transmute(input) }
}
fn ensure_owned_slow(input: Box<BigStruct>) -> Box<BigStruct<'static>> {
    let v = match input.maybe {
        MaybeOwned::Owned(v) => v,
        MaybeOwned::Borrowed(slice) => slice.to_vec()
    };
    Box::new(BigStruct {
        maybe: MaybeOwned::Owned(v),
        lots_o_data: input.lots_o_data
    })
}

#[divan::bench(args = [10])]
fn slow_bench(bencher: Bencher, n: usize) {
    let local_vec = b"12345".to_vec();
    bencher
        .with_inputs(|| {
            let mut inputs: Vec<Box<BigStruct>> = Vec::with_capacity(n);
            for _ in 0..n {
                let maybe = MaybeOwned::Borrowed(&local_vec);
                inputs.push(Box::new(BigStruct {
                    maybe, lots_o_data: [0u8; 1048576]
                }));
            }
            inputs
        })
        .bench_values(|mut inputs| {
        for _ in 0..n {
            let mut dest: Option<Box<BigStruct<'static>>> = None;
            *black_box(&mut dest) = Some(ensure_owned_slow(inputs.pop().unwrap()));
        }
    });
}

You are basically benchmarking (allocation + memcpy) vs (nothing). I think transmute here is fine, but pedantically speaking I would write this to ensure nothing weird happens.

 input.maybe.ensure_owned();
 unsafe {
    Box::from_raw(Box::into_raw(input).cast())
 }

(Generally speaking you probably should not have that big a struct though, just use Vec<u8> or Box<[u8]> or Box<[u8; 1048576]>.)

7 Likes

You are basically benchmarking (allocation + memcpy) vs (nothing)

I chose this particular structure to accentuate the point that the original object isn't being reused. Part of my motivation behind this post was to see if there was a better way to express to the compiler that the existing data structure could be reused, even though the type is changing.

Ideally a way to (safely) rebuild the structure of the new type using fields of the old type that would optimize to the same code as the transmute.

Box::from_raw(Box::into_raw(input).cast())

That's much safer than transmute! Less likely to have unintended side-effects, although still requires unsafe to assert the new lifetime is ok.

Thank you!

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.