Why can't I change a reference?

It's been mentioned in passing a couple of times, but I want to emphasize another reason for &i32 to disallow mutation just like &Vec<i32>, even though it's only an integer.

The reason is optimization, and particularly function-local optimization that does not require full-program knowledge (which may or may not even exist, in dynamic cases). When you pass a &i32 or a &mut i32 to a function, Rust can optimize the way that function uses that reference, reducing and reordering the loads and stores in the program.

For example, in your original obj.bla() example, the closure receives parameter it: &mut i32, derived from a &mut Vec<i32>. But, as a hidden field of the closure object, there is also at the same time an &Obj, from which the closure could derive a &Vec<i32> or &i32, and which it could store somewhere between iterations.

Without making your example an error, or without looking at the definition of iter_mut or for_each or the body of the closure, we cannot know whether they will, say, reallocate the Vec with that &mut Vec<i32>, which could invalidate any &i32s the closure might have created.

But because your example is an error, not only is that sort of pointer invalidation guaranteed not to happen, but we can optimize based on that assumption. Anyone who does happen to hold a &i32 or &mut i32 can cache things it reads, or skip redundant writes, etc, even if those reads/writes happen across completely unknown, side-effecting, global-state-using, dynamic-dispatch function calls.

C++ cannot perform these optimizations, because its const int * type does not have this requirement. What Rust has is more like C's restrict pointers, but checked for correctness at compile time.

This is why we have Cell and RefCell and Mutex and so forth- &i32's immutability is a huge optimization enabler, but when what you want to do would make those optimizations incorrect, you just need some way to turn them off. That's what those types are for- not to make your life miserable because you aren't using the default types, but to let Rust more heavily optimize the default types.

10 Likes

I actually used Indices before, but I tried to parallelize the work using the Rayon crate. Rayon uses the par_iter, so it can split the data for multithreading. My own multithreading approach (4 threads) was slower than single threaded and had these painful Arc and Mutex constructs everywhere. I didn't really like all that bloat as I was just trying to accomplish a fairly simple task.

I guess this is what's called "doing it the Rust way".
When I started with Rust I was fully aware, that some concepts that I have been using in C++ and C all my life will not apply. The main reason for starting Rust were the nightmares of Cross-Platform Compilation, active development and less historical structures that don't make sense.

For my last example I believe now having read the other posts and that it is about the ability to optimise, the simplest way would be to just create a clone. One for reading and one for writing. I am not sure how it performs and maybe it is very inefficient to clone the entire vec if i just want to read a from it. I hope the optimisations Rust can do will then make up for it.

Edit:
Now, I have transformed my old program which took about 60 seconds to complete to one that now takes only 3.2 seconds. I used rayon for multi-threading and did what you people told me: split data into read and write. This simplified the code substantially!

Anyway, I am very grateful for the valuable input, the references, the suggestions, the theory you provided.
I think I am supposed to check a solution, but I think most answers here that make up the solution have been helpful.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.