I have a use case in which I have a struct for which I only have immutable ref available in some methods however I need to mutate a fixed part of that struct. I also want to do this efficiently with as minimum runtime overhead possible as this code is inside hot-loop. I know this can safely be achieved through inner mutability using RefCell however I am not liking the runtime overhead it comes with especially in this case. So I have tried to do something like below (which is just to illustrate the solution, real struct has more stuff in it).
I can guarantee that every time in the methods (which are receiving the immutable ref) I only have to mutate this vec and not anything else, so it is kind of guaranteeing the safety conditions. I want to ask is there any other way achieve this (unsafe code would also work if it's efficient).
Even with unsafe you are not allowed to do mutation through immutable references. The code example you gave contains undefined behavior, and if you run in (in some appropriate main function), the tool miri will also report this fact. You will need to use, at least, the type UnsafeCell in std::cell - Rust for the code to be sound. I. e. vec: UnsafeCell<Vec<usize>>. Then, assuming you make sure that it's safe to do so, you can get the mutable access to the vector via UnsafeCell::get method.
Sometimes Cell can also be more efficient than RefCell, whilst being safe to use without unsafe code being required. I might want to try to benchmark (and/or inspect optimized code for) whether there's much of an actual overhead if you tried safe approaches such as (using RefCell and hoping for the optimizer to see through that, or) using Cell in a .take()-modify-.set() manner.
@steffahn thanks for the response. Yeah I understood the part where immut to mut is undefined behaviour as it’s given in Rustonomicon also. I want to understand whether UnsafeCell has runtime overhead ? Because the code is critical to be performant I am really hesitant to use abstractions which have runtime overhead.
Reading more on Cell, it also looks promising if there is no runtime overhead with it
Only runtime overhead UnsafeCell have is that it may miss some compiler optimizations which are based on assumption that it never be mutated through immutable ref.
So @Hyeonu@steffahn does the below code works fine with no runtime overhead if I guarantee that there will not be simultaneous immutable or multiple mutable references to vec
Yes, but it would be much better to define push_to_vec as an unsafe fn¹ instead of a safe function that contains an unsafe block because it doesn't have any guardrails against being improperly called. That forces the callers to use an unsafe block to call it, acknowledging that there's some unenforced constraint that needs to be manually upheld.
¹ Ideally with a documentation comment describing when it is and isn't acceptable to call.
Thanks @2e71828 for confirming. Also Once all the methods are called I want to move out the UnsafeCell wrapped value and append into another vector. For that I am doing the following thing:
unsafe {
let errors_ref = &mut *self.errors.get();
global_errors.append(errors_ref);
};
where global_errors is the one I want to add the errors into. Is this safe to do given again that I can guarantee that there are no other immutable or mutable references than this. I have mutable ref to global_errors.
I think that this sort of pattern is generally OK within safe functions, given that:
Node is !Sync, so there are no multithreading issues that you need to deal with
You're careful to never let any reference (& or &mut) to the interior of the cell escape from Node's API
While one of these temporary references exist:
You never call an externally-provided closure
You never call into the Node API yourself (because other safe methods may also be accessing the cell in the same manner)
If you're going down this route, these restrictions should definitely be documented somewhere; perhaps in the definition of Node.
This list might not be complete, but any other restrictions should be similar in character; others more comfortable with unsafe may come by to correct me.
Given this pattern of safety requirements, an option would be to use Cell<Vec<usize>> since a Cell by default comes with these safety invariant of "never let a reference escape" and "never call 'user'-code while holding on to a reference", so there's less to document then. (The way the safe part of theCell API enforces this is by never giving you a reference in the first place). The API of Cell features an unsafe Cell::as_ptr method, too, which does offer the unsafe access in the style of UnsafeCell. As I mentioned before, it can also be interesting to compare the assembly and/or performance of that unsafe access, with a .take()-modify-.set() style approach, using safe code.
It may also be worth defining a VecCell<T> type with a safe API to encapsulate all of these safety requirements separately from the other implementation details of Node. This could be based on either Cell<Vec<T>> or UnsafeCell<Vec<T>> depending on your preferences.
In completely safe code, you'd write something like this:
let vec = self.cell.take(); // Replace internal vector with an empty one.
vec.push(item); // Or whatever.
self.cell.set(vec); // Put the modified vec back in the cell.
This isn't as expensive as it looks, because creating an empty Vec doesn't allocate; that happens when the first item is added. So, the unoptimized assembly for this will do something like:
Copy a few words of data (pointer, length, capacity) from the cell into a local variable
Write default values (0,0,0) into the cell
Perform the vector operations on the local variable
Copy the modified vector header back into the cell, overwriting the defaults written in step (2)
In most cases, depending on the details of (3), the optimizer should be able to realize that nothing observes the contents of the cell during this and replace it all with directly operating on the Cell's internal memory.
ohh that's interesting @2e71828 . That means it does not have runtime overhead and it can push elements with immutable reference. I guess this seems more promising to implement rather than using unsafe code. Is there any advantage of the unsafe code given I get it right ?
Well, there's no actual guarantee that the optimizer will recognize that it can remove the copies. Your unsafe version always acts directly on the vector in the UnsafeCell and will likely be ~5-10 words of copying faster than the unoptimized Cell version.
On the other hand, the failure case if you mess up your analysis and external code actually does see into the vector while you're working with it is very different between the two versions:
In the Cell version, that code will see an empty vector and any changes it makes will be discarded when you overwrite the temporary in the set() call
In the UnsafeCell version, your program has executed UB and may start to exhibit arbitrary faults.
Given these tradeoffs, and the relative expense of a potential vector reallocation compared to the Cell's worst-case cost, I'd personally go for the safe Cell version every time.
actually @2e71828 my vector is vector of errors I am collecting over an AST pass for some static analyzer. Everytime an error occurs it is pushed into that vector. It is guaranteed to have just one mutable reference at a time, pushing the entry and returning. The whole program runs on single thread and there is no immutable ref required anywhere in the code for errors vec (because there is no need, it's just dumping storage). And because it's any AST pass, it needs to be highly efficient and I want as little runtime overhead as possible even if I have to use unsafe code.
For a pattern like that, I'd probably not use interior mutability at all. Instead, I'd pass around an &mut Vec<...> as a function argument during AST traversal. If you need several different accumulators like this, you can define some kind of struct Cursor so that you can forward all of them with a single argument.
Actually previously I was doing this only, that there was a struct TypeChecker and in all the check methods for various nodes I was passing mutable reference, but then I had to eliminate some Rc and starting working with immutable references which rust had problem as code was written so that it works with owned values. Now once it got converted to immutable references instead of Rc clones, there were mixing of mutable refs and immutable refs. I observed that the only thing I want mutable ref was for logging errors and nothing else. So I thought I can have my type-checker work with immutable ref but have inner mutable errors. I also wanted this to be efficient. I traded Rc clones with immutable refs in first place for efficient traversal only. Intuitively also It makes sense to me that type checker is there just to check type errors and should be okay with immutable refs unlike resolver which needs to fill scope table and all, it’s just error logging which made it mutable.
Unfortunately, that's only true if the optimizer knows that the code between Cell::take and Cell::set doesn't panic. But a vector can always panic due to reallocation, thus the creation of intermediate vector likely won't be elided (playground):
use std::cell::Cell;
pub fn push_vec(v: &Cell<Vec<u32>>) {
let mut v_inner = v.take();
v_inner.push(2);
v.set(v_inner);
}