Unfortunately, this model really rubs me the wrong way. In particular, the fact that refactoring across modules is broken and the fact that a single unsafe block suddenly deoptimizes a whole module bother me. Moreover, I don’t see this as a necessary evil - in both of the motivating cases that you brought up, I feel that the code is correct for completely different reasons than what you describe.
Let’s consider the refcell-ref
case first, since this is what led you to broaden the unsafe boundary to whole modules.
In this case, you consider the optimization of reordering
let t = *self.value; // copy contents of `self.value` into `t`
mem::drop(self.borrow); // release the lock
t // return what we read before
to
let t = *self.value; // copy contents of `self.value` into `t`
mem::drop(self.borrow); // release the lock
t // return what we read before
and conclude that “reordering this code would be an invalid
optimization. Logically, as soon as self.borrow is dropped,
*self.value becomes inaccessible.” However, I think that this is the wrong conclusion.
In the single-threaded case, I would expect and want the compiler to do the reordering - it might be breaking the abstraction, but it would still be correct, and it would likely be faster. A lot of the power of a compiler in general comes from breaking abstractions - this is why inlining is so powerful. We shouldn’t bound the optimizer by what, intuitively we think about the accessibility of values - we should instead bound the optimizer by when values actually are accessible.
In the multi-threaded case, you are completely correct that the optimization is invalid. However, we don’t need to put any new constraints on the optimizer to prevent the reordering - drop(self.borrow)
will contain a memory fence that prevents reordering the load with it. Thinking about the implementation of a lock, when the lock is freed, some memory has to be written or some message sent that announces to other threads that the lock is free, and that write/message send will have a Release
memory ordering that prevents reads from being moved across. Generalizing this argument, it isn’t hard to see that in any case where a piece of memory becomes inaccessible due to interference from another thread, there must be a memory barrier. If there wasn’t, then other threads would have no idea whether the main thread has released access to the memory, since communication could be arbitrarily reordered and delayed.
Therefore, there is no reason to include copy_drop
in the unsafe boundary or hamstring optimizations in it.
Now, let’s consider the split-at-mut-via-duplication
case. Although I originally thought otherwise, I agree that this code should be safe. As a stronger example, in the current implementation of BTreeMap
's iterators, all the mutable iterators store two mutable pointers into the tree, gradually moving one towards the other. In this case, it is impossible to do what @Ericson2314 suggested, wrapping the use of the two mutable pointers in an unsafe block, because the use of the pointers is in the next
function, which is necessarily out of the standard library’s control. However, I don’t agree that the whole split_at_mut
function has to be in the unsafe boundary - in fact, I’d argue that copy
and self
don’t alias.
To some extent this is quibbling about definitions - what does it mean for two pointers to alias? However, it has huge effects, and I think the definition that you gave (pointing at overlapping regions of memory) is highly flawed. I would define aliasing as follows:
Two pointers alias if there is a write through one of them to the same location that there is a read or write through the other pointer.
If I remember correctly, this is also the definition that LLVM uses. Note that this is a property not only of the data, but of how the data is used. Fundamentally, I think that the reason duplicating &mut
pointers is unsafe
is not because the two resulting pointers point to the same data, but the fact that, done in a public function, you can’t know what the user will do with those two pointers, and they might write to both, causing aliasing.
Beyond being more lenient, this definition matches what optimizations the compiler actually wants to make. The compiler uses noalias
information to reorder loads and stores, which only requires that other loads and stores don’t get in the way, not that there don’t exist any other pointers to the same location.
This rule for aliasing also reminds me of an annoying rule in C, and how it might be fixed. In C, you can’t create a pointer more than one element beyond the end of an array. This is really painful, and a much nicer rule is to just say that you can’t dereference such a pointer.
From this perspective, self
and copy
don’t alias, as they are immediately adjusted to point to disjoint memory locations, and they are never used to access shared locations. Thus, the function is safe because at the end of the unsafe boundary (the end of the actual unsafe
block, not some far-reaching scope) there are no aliasing &mut
references.