I find the notion of an unsafe block deoptimizing nearby safe code distressing, because it goes against my expectations of how unsafe works and how I’ve been using it. In general, I think that if a choice needs to be made between unsafe changing how nearby safe code compiles and unsafe code more subtle than C due to stricter aliasing rules, I would prefer letting unsafe be more subtle than C over letting unsafe have a blast radius affecting how nearby safe code compiles.
As a user of the language, here are some of my expectations (all of these map to actual or, in the case of reinterpreting slices’ pointers as pointers to larger types, near-term planned uses of unsafe in encoding_rs or encoding_rs_compat).
I would expect a transmutation of a u32 to char to have truly zero cost, including in terms of lost optimizations, and I would find it very astonishing and unwanted if this kind of unsafe deoptimized anything nearby. Likewise, I’d I’d expect a transmutation of &[u8] to &str to have truly zero cost, including in terms of lost optimizations. (In fact, before reading the posts here, it didn’t even occur to me that this kind of unsafe might involve lost optimizations nearby!)
I would expect making uninitialized memory available for writing to have a negative cost: I’d expect it to save the cost of zeroing the memory. Again, I would find it astonishing and unwanted if this kind of unsafe deoptimized anything nearby.
The above examples don’t really involve pointer manipulation, so my main point with these examples is that since unsafe in Rust is not qualified with an annotation about what kind of safety is being moved from the responsibility of the compiler to the responsibility of the programmer, it would be particularly bad if the presence of pointer-unrelated unsafe to contaminated the module (or anything nearby) with pessimistic assumptions about pointer aliasing.
Moreover, due to the way unsafe has been characterized in documentation as allowing you to do certain things that are otherwise prohibited, I would find it surprising if marking any pre-existing block (so as not to introduce new scope boundaries) of safe code as unsafe changed the semantics of the code, since such a change wouldn’t introduce any otherwise prohibited actions. If marking an existing safe block unsafe doesn’t change its semantics, having the seemingly no-op annotation deoptimize code behind the scenes would be astonishing and unwanted.
Even with pointers present, if I get a handful of pointers over the FFI and I go ahead and just dereference them (without null-checking), I would expect the generated code to perform as well as it would if those pointers had been references. That is, if I write in the FFI documentation that the pointers must be non-null and must be disjoint and then write dereferences with this assumption, I’m not expecting the compiler to insert pessimism for me.
If I write a function that takes two slices of small integers, say &[u8] and &[u8] or &[u8] and &[u16], and then I decompose those to pointers and lengths and reinterpret the pointers as pointers to larger ALU words or as pointers to even larger SIMD types and read via the pointer that reinterprets one slice and write via the other pointer that reinterprets the other slice, this reinterpreted pointers are still disjoint and I wouldn’t want each write to deoptimize the next read with pessimism about the pointers now may be aliasing.
So my conclusion at this point is that I’d expect unsafe not only to make it the programmer’s responsibility to make sure that Rust’s safety invariants have been restored by the time control exits the unsafe block but also to make it the programmer’s responsibility to uphold Rust’s pointer non-aliasing assumption.
Which would make unsafe even more subtle in terms of UB than C.
Then, going back to the Tootsie Pop examples, I conclude that if an abstraction internally uses unsafe, it may be necessary to introduce additional unsafe in some of the operations that could get messed up under a model where unsafe blocks never affect how safe code is compiled. That is, in the refcell-ref example, I’d rather require copy_drop to use some explicit unsafe operation order forcing mechanism than to have unsafe blocks in a module pessimize safe code.
This means that some internally-unsafe abstractions, such as RefCell, become even harder to write correctly, but hopefully that kind of code will mainly live in the standard library and be written by people who know the subtler-than-C rules. Then, it would probably be appropriate to make other users of Rust aware of unsafe not being just syntactically different C (if the aliasing rules are actually more like those of Fortran) to discourage people from writing large unsafe blocks as if writing inline C.