Unsafe code vs. dancing with the optimizer

⚓ Rust    📅 2025-09-22    👤 surdeus    👁️ 8      

surdeus

Warning

This post was published 39 days ago. The information described in this article may have changed.

I peeked at the recently-const-stabilized slice reverse source code. Here's what it looks like:

#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_slice_reverse", since = "1.90.0")]
#[inline]
pub const fn reverse(&mut self) {
    let half_len = self.len() / 2;
    let Range { start, end } = self.as_mut_ptr_range();

    // These slices will skip the middle item for an odd length,
    // since that one doesn't need to move.
    let (front_half, back_half) =
        // SAFETY: Both are subparts of the original slice, so the memory
        // range is valid, and they don't overlap because they're each only
        // half (or less) of the original slice.
        unsafe {
            (
                slice::from_raw_parts_mut(start, half_len),
                slice::from_raw_parts_mut(end.sub(half_len), half_len),
            )
        };

    // Introducing a function boundary here means that the two halves
    // get `noalias` markers, allowing better optimization as LLVM
    // knows that they're disjoint, unlike in the original slice.
    revswap(front_half, back_half, half_len);

    #[inline]
    const fn revswap<T>(a: &mut [T], b: &mut [T], n: usize) {
        debug_assert!(a.len() == n);
        debug_assert!(b.len() == n);

        // Because this function is first compiled in isolation,
        // this check tells LLVM that the indexing below is
        // in-bounds. Then after inlining -- once the actual
        // lengths of the slices are known -- it's removed.
        // FIXME(const_trait_impl) replace with let (a, b) = (&mut a[..n], &mut b[..n]);
        let (a, _) = a.split_at_mut(n);
        let (b, _) = b.split_at_mut(n);

        let mut i = 0;
        while i < n {
            mem::swap(&mut a[i], &mut b[n - 1 - i]);
            i += 1;
        }
    }
}

I noticed that there's quite a dance done between the author and the optimizer to avoid both calling unsafe functions and paying the penalty of bounds checks. See, in particular, the creation of the a and b slices.

I wonder if implementing such a function using lower-level unsafe pointer arithmetic would be an improvement. There's more unsafe code, but it more directly conveys the programmer's intent and avoids dependency on specific optimization passes, which I assume aren't guaranteed.

I'm new to Rust, so I would be very interested in hearing experienced folks' opinions here.

4 posts - 4 participants

Read full topic

🏷️ Rust_feed