Is there an optimization difference between UnsafeCell<T> and *mut T? When should they be used, and what should be kept in mind?

I'll also note that you implicitly introduced & and &mut multiple times in your snippets.

        // Creates a `&a`
        println!("{}", a); 
    // These method calls take `&mut _` / `&_`
    let b = a.as_mut_ptr();
    let c = a.as_mut_ptr();
    let d = a.as_ptr();
2 Likes

So for raw pointers obtained directly (without going through a reference) using the &raw mut syntax, it is allowed to have multiple &raw mut to the same memory, provided there is no &raw const declaration that tells the compiler this memory will not change while the &raw const is still active, because if it does change, that would be Undefined Behavior? But if all of them is &raw mut pointer then one of three &raw mut pointer is cast to *const T, is that allowed because there was no initial contract stating the value would never change for a specific duration (the duration of the &raw const)?

Regarding optimization, if I create a raw pointer directly, it won't get the optimizations that references receive. Then, casting that raw pointer back to a reference would make it eligible for optimizations again. What about pointers derived from references? For example, &mut T as *mut T, does it keep the optimizations because it was previously validated through a reference or not?

Thank you, I just found this out. So println! creates a &T under the hood, which makes all those previous examples Undefined Behavior. As for .as_mut_ptr() it creates a *mut T from a &mut T, which causes Undefined Behavior if called on the same variable while another .as_mut_ptr() to it is still active.

I saw the source code, it uses &mut self, then casts it to *mut str, and finally to *mut u8. Is there a reason why it has to go to *mut str first instead of directly to *mut u8?

pub const fn as_mut_ptr(&mut self) -> *mut u8 {
        self as *mut str as *mut u8
}

Furthermore, when I tried to make a version using &raw mut, using &raw mut T as *mut u8 as shown in the code below, it didn't work. Miri reported Undefined Behavior: incorrect layout on deallocation. If I go through go through *mut str first, like in the std source code, using &raw mut a as *mut str as *mut u8, it gives an error like this: cannot cast thin pointer *mut std::string::String to wide pointer *mut str. Why is that not 'apple to apple' (an equivalent comparison)?

The working code :

fn main() {
    let mut a = String::from("a");
    
    let b = a.as_mut_ptr();
    
    unsafe {
        *b = b'b';
        println!("{}", a);
    }
    
    let c = a.as_mut_ptr();
    
    unsafe {
        *c = b'b';
        println!("{}", a);
    }
}

The error code :

fn main() {
    let mut a = String::from("a");
    
    let b = &raw mut a as *mut u8;
    
    unsafe {
        *b = b'b';
        println!("{}", a);
    }
    
    let c = &raw mut a as *mut u8;
    
    unsafe {
        *c = b'c';
        println!("{}", a);
    }
}

Yes.

No, that would be incorrect for the optimizer to do.

Think of it this way: if you have some reference r: &mut T, then any valid means of conversion of it to a raw pointer is kind of like like the reborrow &raw mut *r. That is,

  • r points to some place *r, and because r is an exclusive reference there are some rules about what you can do with that place and what you can assume is done with that place — in particular, as long as you hold that exclusive reference you know nobody else reads or writes, and when that exclusive reference's lifetime ends, you may not access that place any more.
  • One of the things you can do with *r is borrow it, such as let p = &raw mut *r;.
  • Once you have that raw borrow, you can use it to do any of the things that you could do with *r, but may not do any things that you couldn’t have done with *r (such as access it after the reference lifetime expires, or access memory past the end of the T).

Insofar as you are constrained in what you can do with that *mut T, they are not restrictions on the pointer, they are restrictions on what you can do to the place *r, which exist regardless of whether you use *mut T or some other kind of access, and do not say anything whatsoever about how many copies of *mut T you use.

Note that this is the code for str::as_mut_ptr(), not String::as_mut_ptr().

This is because as casts don't actually let you do all possible conversions in one step. The first one is a reference to raw pointer conversion, &mut str to *mut str, and the second one changes the pointee type from str to u8.

You’re converting *mut String — a pointer to a String, which is a struct containing a pointer to str and two length values — into a pointer to u8, and writing to that. So, you are corrupting the pointer or the length, not writing to the actual text. This has nothing to do with borrowing or provenance rules; it is entirely about what data the pointer actually points to.

1 Like

Thank you, that clears up my confusion