Using a move semantic - guaranteed penalty?

I'm using Mmap that facilitates sizing a large chunk of memory for purposes of "divide and conquer" approach to processing.

I have a method that really only needs to reference the Mmap but for my Rust skills, have not successfully navigated how to decode the bytes to &str without having to take ownership of the Mmap.

impl Tape {
   /* .. */
  pub fn view_record(&mut self, record_idx: u32) -> Option<&str> {
      Some(unsafe { std::str::from_utf8_unchecked(&self.memmap[*mem_start..*mem_end]) })
  }
}

The above is a method for the following

pub struct Tape {
    tape: Option<Vec<Record>>,
    index: StructureIndex,
    memmap: Mmap, // takes ownership
}

Separately, every now and then I find myself gravitating to a functional style where after moving the input into the body of the function, I will return it, perhaps as part of a tuple with the product of the computation. The only reason to do so, is to make the subject of the computation explicit in the return value (e.g., fn (state, action) -> state -- no side-effect) . Notwithstanding, there are two scenarios, with or without mutation.

The two-part question:

When moving ownership in and out of a function

A. without mutation,
B. with mutation

...is there any reason to believe there is a performance penalty compared to letting the function consume a "borrow" of the data?

- E

What error does view_record fail with?

@alice The following code

error[E0308]: mismatched types
  --> src/tape.rs:48:53
   |
48 |         Some(unsafe { std::str::from_utf8_unchecked(self.memmap[*mem_start..*mem_end]) })
   |                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                                                     |
   |                                                     expected `&[u8]`, found slice `[u8]`
   |                                                     help: consider borrowing here: `&self.memmap[*mem_start..*mem_end]`

The fix is to do exactly what the compiler says.

Some(unsafe { std::str::from_utf8_unchecked(&self.memmap[*mem_start..*mem_end]) }
                                            ^

...compiles and behaves as expected.

Before asking this question, I tried to proceed by dereferencing the borrow:

Some(unsafe { std::str::from_utf8_unchecked((*self.memmap)[*mem_start..*mem_end]) })

... where, the fix is to "reborrow"; a redundant task given the above fix.

The reason for taking this approach, and getting tripped-up accordingly, was my prior success in avoiding a move semantic for another function that also uses the Mmap instance:

pub fn read(memmap: &Mmap) -> StructureIndex {
  //...
  let (head_u8, body_vector, tail_u8) = unsafe { (*memmap).align_to::<__m128>() };
  //...
}

The inputs to each of these functions seem to be one in the same; &self and &[u8] given that self is the Mmap instance.

pub unsafe fn align_to<U>(&self) -> (&[T], &[U], &[T])
pub unsafe fn from_utf8_unchecked(v: &[u8]) -> &str

Can you put words to how self and v bind mmap input differently? I believe this might be where I need to adjust how to think about it all (the performance question notwithstanding).

Autoreferencing and auto-dereferencing happens for the receiver of methods (self). If the method takes &self but you have a value of type Self, then it's automatically borrowed. This does not happen for non-self arguments: if you have an argument of type &T and you provide a value of type T, that's a compiler error.

Got it. That means I should be able to compile without derefencing the borrow in the body of the read(memmap: &Mmap)... function:

let (head_u8, body_vector, tail_u8) = unsafe { memmap.align_to::<__m128>() };

... compiles. Cool.

It's coming together: without this functionality I would have to write (&instance).method() to bind the return value of the method.

1 Like

Just to close out on the topic, my conclusion is that a move semantic is not less performant compared to a borrow. Yes?

No need to read on. What follows is just my consolidation of the concepts.

Memory for a new reference is consumed (with ownership privileges) but the memory for the value itself remains unchanged. From a performance/memory consumption perspective, mutation is what needs to be considered. In that regard, &mut value and a move that ultimately gets cast to mut value are one in the same from a memory and performance perspective.

In addition to the code I was dealing with, part of this thought process was triggered by my reviewing why a structure cannot have a reference to itself. What links these disparate "universes" of thought, like most things, is not only what is happening to the underlying memory, but what is permitted and thus what might happen to the underlying memory (so much in logic is captured in what is not specified i.e., where everything else exists).

Ownership and a reference to that ownership are incompatible when bound together (as is the case when in the same structure). Bound together in that their lifetimes are one in the same, but can be accessed independently of one another. Taking ownership of the structure confers the option to mutate the value. By default*, Rust has the right to change the location of that memory. That change in memory could invalidate the reference with a lifetime tied to the value.

This logic is obvious when replicated in context of a function. A function that allocates memory to instantiate a value, can return that value (a move semantic). A function cannot however, return a reference to newly allocated memory (well understood). Nor can the function return a tuple with the value and a reference to that value (or to anything inside that value). The compiler complains about returning a reference to memory allocated in the body of the function. The same message as when trying to return the reference on its own.

As mentioned, well understood on its own, but somewhat obfuscated logic when simultaneously moving the value. The logic is more transparent when considering what ownership confers (the capacity to mutate), and what mutation permits (the default* option to reallocate memory). The struct context encodes a "product", more than one thing simultaneously (in contrast to "sum"/enum). Does Rust not coordinate a unified view of a struct? No. In the function that returns a tuple, the consumer: (value, borrow = wont_compile_fn() clearly has the ability to mutate the value whilst the borrow never "receiving the memo" i.e., being made aware of the updated address. This is where my question at the very top relates to this topic. The possibility for UB is not by definition of a move; a move does not mean the memory for the value will be reallocated; or is even likely at all to be reallocated. If I stopped here, it should be ok to define a struct that has a reference to itself.

The final unification of the Rust universe, is the relationship between a move and a &mut T. How is it a &mut T can change the underlying value and consequently maybe change the memory location, without invalidating it? Using the same analogy, a memo does not need to be sent because we are not trying to use 2 references to the same value. Where before the capacity to mutate was encoded in the reference:owner and the capacity to borrow encoded in a separate reference:borrow, the single reference:mut_borrow (&mut T) encodes both capacities (sufficient ownership in that moment* and mutation). If and when the memory address changes, by definition of how this capacity is encoded, Rust can both update the memory and the reference used to access that memory (addendum: The compiler also invalidates any other &T and &mut T instantiated prior to this event; this invalidation view is another angle/reason why a value and ref to that value cannot coexist in a struct).

So, no performance hit for a move semantic in itself. I can control when to take a hit; i.e. when I chose to mutate. For completely different reasons, it is permitted because I'm only ever using a single ref/alias. However, I can't use the same logic to build a struct that includes a reference to itself -- even if I don't exploit the opportunity to mutate. From the movie Jerry McGuire: "you had me at hello" - Rust "lost me at" wanting to borrow and mutate with two separate aliases to the same memory, living simultaneously" which is what happens in the struct.

* "default" memory allocation behavior. In my understanding, this is where the Pin trait is useful and critical. The details of the logic are as intuitive as a NotAnd logic gate, however, the effect is straightforward. The compiler can be made to mutate memory whilst promising not to change the memory location. If memory serves, to the degree the async capacity relies on futures, Rust relies on every object in Rust implementing Pin to enable the toggling of this memory allocation behavior (where the default is "free"... a la only pay for what you use).

Since in practical code, moves and borrows are almost always very cheap (almost free), and opimizers are smart, it doesn't make much sense to ponder the performance impacts of moving vs borrowing. However, there can be subtle differences, involving a bunch of factors, some of which are the (maybe somewhat surprising) results of modern computer architecture:

  • Memcpy'ing large values can take measurable time if not optimized away. So if you are moving a [u64; 1_000_000] in a loop in a debug build, you might see that it's slower than not moving it would be.
  • Missed optimizations can happen in unexpected situtations. Sometimes, return value optimization doesn't kick in, sometimes the size of the code hits some internal heuristic threshold of the compiler, causing it to give up on further eliminating memcpy()s, etc.
  • However, referencing isn't unequivocally "faster", either. Indirection has a cost, since it may incur a cache miss, potentially leading to something like a 10 or 100-fold slowdown. Especially if the reference points to the heap (as opposed to the stack), since the stack tends to be "hot", and as such, stack memory can have a higher chance of being in the cache at a given point in time.

Still, all of the above is mostly a non-issue – during my years with Rust, I don't recall ever having seen a noticeable performance problem that was fixed by borrowing instead of moving or vice versa.


Some comments about the second part of your post:

Not sure what you mean by this. Referencing does not consume the memory occupied by a value. Do you mean "by-value binding" or "moving" instead of "a new reference"?

Mutability does not affect either the running time or the memory consumption of the code. It is merely an annotation for humans, enforced by the compiler, as a reminder that we should think twice before mutating.

This is not really true in terms of the mental model, but it is true that a lot of problems can be formulated either in terms of a mut owned value or a &mut reference, so that the results are equivalent. It is important to note though that sometimes it just doesn't make sense to use one or the other. For example, if a function has the semantics of mutating in-place, it's a lot more cumbersome to take and then immediately relinquish ownership, while also making it impossible to use such an API from code that only has access to &mut references.

I think you are reading too much into that. Conceptually, moving from a value invalidates it at compile time, the primary purpose of which is to prevent a single value from being dropped multuple times, erroneously. If you do let s1 = String::from("foo"); let s2 = s1; then running the destructor of s1 leads to memory corruption even if s1 wasn't underhandedly mutated by the compiler. In fact, the very fact that s1 and s2 contain the same pointer to the same heap buffer causes the double-free. The fact that the memory region previously occupied by s1 may be reused for another value is an implementation detail and is irrelevant from the point of move semantics.

(Incidentally, do note how sharply this differs from the approach of C++: in C++, moving dynamically invalidates the moved-from value, resetting resources to an "empty" or "no-op-when-destroyed" state. That's why move constructors need to accept a non-const reference.)

Are you confusing invalidation arising out of a move with "iterator invalidation" here? There are two distinct things going on, however, one of them (moving) has nothing to do with mutability:

  • If a value is moved from one location to another, then the original location is invalidated, for the reasons described above. The original location is not changed, nor does the new location, nor is it a prerequisite for memory corruption that the contents of either of them are being changed (again, like I just explained above). The point in not allowing a currently borrowed value to be moved from is that having a dangling reference is a compile-time error in Rust, so making a previously valid reference dangling should be a compile-time error, too. (And invalidating the region pointed to by a reference is pretty much the definition of making a dangling reference.)
  • When mutability comes into play is dynamically-allocated data structures. A data structure, such as a vector or a tree, might want to actually move stuff around as a result of its structure being changed. For example, a vector might need to reallocate when push()ed to, and a self-balancing tree might want to split or merge nodes when insertions or deletions occur. This would most likely result in actual memory addresses changing and real memcpy()s happening to different locations, invalidating any previous pointers into the internal allocations of the data structure. That's why mutably borrowing an already-borrowed value is prohibited.

A &mut T cannot change the memory location of the value being referenced. If you have a mutable reference that points to a value at address 0x1fff and you change the contents of the value by dereferencing the mutable reference and assigning to it, the underlying pointer will still point to the address 0x1fff. It's only the (however many) bytes starting at 0x1fff that will change.

I'm not sure what you are picturing here. Since writing to the value behind a mutable borrow does not change the address of the borrow, there is no need for any memory addresses to be "updated".

3 Likes

Your top line conclusion is what I will take as the "dominant" way to think about it. What you have described here goes to the heart of my "fear"; is it at all signaling "pass by value". If what I go on to describe is correct, I don't see how/why the compiler would copy memory differently. Unless, as you also describe, a little less indirection allows the the compiler to "see" an opportunity to optimize where otherwise it would not.

No. Apologies for the ambiguity. I'm being a little provocative with how I'm describing the situation... to promote a systematic/unified train of thought. The memory being consumed that I'm describing is only that for the reference. To accomplish the task of conferring/distributing a "read" capacity to other scopes, it's a mental model for reminding myself I am always copying something (at least before optimization) and thus at some point either consuming it locally, or moving it out of the local scope. It's always a zero sum game involving some amount of memory; the only decision is memory for what. A borrow, is a copy (and often a move) of a reference; the reference itself requires memory. Accounting for memory, when the memory for the value itself is small compared to the memory for a reference, what gets copied is "six half dozen"; in a read scenario, Rust truly doesn't care to the point where it will go ahead and copy if I "forget" and try to borrow what was moved, Rust will do me "a solid", create a second copy of the value to make the new own ref just that, and valid.

This process is exactly what goes on with borrows to non-primitives; same calculus where, only in the event I "neglected" a move semantic, the conclusion is instead "this is not six-half-dozen; the user needs to make a decision on how to proceed". The key, the compiler will move the owner reference but the borrow rules prevent it from creating a copy of own ref because it can't create a copy of the value itself for the "would be new" own ref (to a new value). It can't do so without the Copy trait implementation. While that explanation is interesting and hopefully mostly correct, it's a derivative of the true cause: I'm trying to copy the wrong ref type. Otherwise, like primitives, borrows are copied "as needed". AND further, my passing a reference to be consumed by a function is a move, just a move of the reference (again, copied as needed). So when sharing anything with a different scope, something is always being moved; the question is what, and how that impacts the ability to "continue" to use the value itself in the current scope. Rust suggests "make another copy" so that the own ref you have here can point to memory that I can make promises to you about. More often, not the intent.

I used the word "consumed" (aka dropped) because I want to complete the accounting cycle as I would when tracking any memory allocation... and a basis for thinking about lifetimes. Hopefully this will promote a single, unified/consistent thought process. The above I think captures a good chunk of the "read" memory world.

If my statement connotes "negative performance", I understand your point. More precisely, incremental memory allocation needs to be considered. There is always a cost/time to allocate new memory. Only to the degree the human mutation tag "opens the door" to a new allocation (a growing Vec) do I need to consider. To your point, I believe it can take more cycles to load data into a register compared to storing it!; so nothing inherently one way or another from a performance perspective. Am I still misunderstanding?

... and, I'm promoting the idea that because a function can effectively "recast" a parameter with a move semantic to one with mut privileges (by creating a new reference; see Playground), the "problems in terms of a mut owned value" only need to be owned. I argue, the logic must be as describe the complete universe. For instance, without this inclusion, I can't explain why I "can't have a struct with a reference to self". It's "dead on arrival" even without my explicitly signaling an intent to mutate... "that ship sailed" :)) every time I instantiate and take ownership of the structure.

Given the above clarification, am I still missing something?

Understand and agree. However, this comment helped me put my finger on the source of the block. The way you stated it here is exactly the Rust mantra. It is useful and I get it. However, "moving from a value invalidates it". The proper understanding hinges on what is the "it". We do move something, and we do invalidate something; the reference, not the value. The distinction is massive and likely so obvious to most (especially the C++ audience).

Earlier you suggested I might be referring to "call by value". You might be right. Of course I understand it; I can explain what it means, but I believe my subconscious associations are a source of tension and confusion (I did not just say that :)).

In JS/React I'm encouraged to use functions that perform a "shallow copy". In Haskell's default, pass by value, pure, no mutation (optimized under the hood). In C the default is call by value etc... Even knowing that, and habituated accordingly, I was likely "crossing-wires".

Here is where I could read it, but not get it. The current passive, ingrained associations were insufficiently actively contrasted with what is going on with Rust.

  1. Thinking that the contrast between a move and a borrow is the same as the contrast between a call by value and by reference. Pass by value ensures the function won't change the value passed to it => copy => "expensive". Flawed, need more...

  2. C is pass by value. It avoids the expense using pointers ==>> dangerous if not careful. Rust has a solution to avoid both "expensive" and danger => borrow checker. Cool, let me try...

  3. Often the first (and lasting) impression of the borrow checker occurs when when our experiments using primitive values fail to compile with non-primitives: "can't move because does not implement Copy". This experience interacts with prior associations in unhelpful ways ("crossed-wires"):

    A. Note the association: tried to move => can't because does not implement Copy

    crossed-wires: oh, to move, must copy... just like what happens when pass-by-value, pass-by-value => Rust move semantic

    B. The solution is to borrow => avoids having to implement copy

    crossed-wires: move => copy and explains the difference between borrow and move (oh, that's why it's called move, I can pass-by-value with a move)

In working through the gaps, I can't help but echo what is consistent with the following:

C is call by value. Rust is different => Rust is pass by reference
C uses pointers to pass by reference => Rust uses a borrow checker

The most glaring of gaps, something I could speak to, but not always reflect, is how a reference with a move capacity has nothing to do with pass-by-value despite how it might look like what is done in C. A C pointer is the "star villain" in understanding the "hero" in Rust :)), but what's missed is how the C variable has no useful parallel concept in Rust. The Copy trait exists, but thinking about that will not help understand Rust. I resort to a some sort of "copy" when I get in trouble with the borrow checker. Truth be told, for me, I can't help but try to figure a way out of the failed "sniff test". I see it as a "cheat" less to do with a lost performance opportunity, but more because it's conceptually going to block a better understanding of the language (not just an itch, but a tried and true experience with code that lasts).

Summary baseline dynamics:

  1. memory allocation happens once for the values you care most about (i.e., the "data proper" if you will); you have to go out of your way to copy if it's not already done for you. It's not idiomatic, nor expected. The essence of a failed move, rarely has to do with an inability to Copy; it's true in the micro, but it's likely because you're trying to copy the wrong thing; what can't be copied, likely has a ref that can (something Rust will do for your because it is the dominant choice - nothing to decide, so completed on your behalf).

  2. move, borrow are both "pass by reference", they both can be dereferenced to view the value to which they refer. The refs themselves also consume memory; this memory obeys the same rules as that of the value to which it points. Any accounting that appears different is likely explained by the dominant choice Rust made to copy the value to avoid a move semantic error (value: primitive | reference).

  3. lifetimes of these references only matter when they are not inherently tied to when the memory for the value is dropped (so not an issue for the move/owner ref). Rust confers that unalienable capacity to the ref used to instantiate the value and subsequently, to the chain of ref involved in any move semantics.

  4. Rust can only maintain the viability of a single move ref through all changes in memory (including a change the memory address or size for the value); to effect this rule Rust will invalidate the current move ref when "asked" to create a new one. Any permitted changes to the underlying memory will be recorded by this single owner ref.

  5. Any other ref to memory that can mutate, must satisfy (a) not existing longer than the memory to where it points (b) cannot share a scope with other references to the same value (where scope can include a sequence of expressions in the same scope that change the value "between" reads). The validity of the mut borrow ref and the owner ref is assured by Rust updating any changes in address (and size where applicable) to the underlying memory. Any other references to that memory are invalidated by definition of the mere potential for mutation. This includes a read capacity to memory with ownership that has been moved out of the scope, even temporarily and even without an explicit mut tag. This invalidation is required because an own ref can be cast to mut ref (both type own), Rust can no longer assure that the memory holds the same value (mutation between reads is not allowed) or worse, was moved (the old address to where the ref points might yield a valid, unchanged value, but when least expect it, read memory that has be reallocated). Again, this holds even when ownership is subsequently returned to the scope.

  6. While how memory to a value might need to be specified, the user likely never has to explicitly trigger the drop function defined in the drop Trait. This is "for free" memory management triggered precisely when the reference falls out of scope. So, no need to concern with a "double free".

  7. A side effect that might not be welcome (that I can think of in context of this scope), is the inability to use the same reference (single instantiation) to read memory "between" mutations. The read ref must be renewed/reinstantiated following each mutating event. Rust's requirement for this explicit intent affords extra safety on the stack and an extra degree of freedom when managing memory on the heap (unlike C, Rust calls malloc, free and friends under the hood and thus at it's precise discretion).

To be honest, I'm not sure anymore. This is getting way more complicated than it is (or needs to be). I don't think considering loads and stores is relevant when discussing mut – because, again, mut is just an annotation that does not change the meaning of a correct Rust program. If a Rust program compiles, you could add muts to all bindings (and cope with the sea of warnings), and it wouldn't change its behavior or performance a bit. The compiler could also be instructed to just treat every value as mutable and you could then remove all the muts and programs that would otherwise fail to compile only due to the absence of mut would compile and run as expected.

That's not what I'm arguing. I'm saying that if you have a function that takes &mut T, then there's no way you can pass (== move) the pointed value to a function that takes a T. I.e., you can't move out from a reference. This is for good reasons (e.g. it would leave the original place of the moved value invalidated, creating a dangling pointer), and as such, it can't just be ignored.

Again, this might be reading too much semantics into what you mean by "reference" or "value". I like to picture this as two boxes with labels. Boxes are memory regions, labels are their addresses, and whatever is in the box is a value. So, if you move a value from one memory region to another, then the first box (memory region) is invalidated, and the second box gets its value (or a copy of it). Their labels don't change; it's just that you are not allowed to use a label of a box that is considered invalid, and you are not allowed to take a value from a box while someone is holding onto its label.

The difference between moving a value into a function argument and passing a reference to a value in a function argument is exactly what is meant by the by-value vs. by-reference distinction. No trick questions there.

Except that this copying usually just isn't; and even if it exists, it's mostly trivial. The primary purpose of references in Rust is not to spare trivial copies. It is to provide indirection, because it is not always possible to reason with ownership (e.g. recursive data structures are impossible to realize without indirection).

Incidentally, the same is true in C. Rust references are pretty much just the same as C pointers except that they are more richly typed and consequently they can be (and are) checked for certain criteria of correctness.

No, Rust is pass by value as well. As I wrote earlier, in another thread, pretty much everything can be considered a value in Rust, and, as you correctly mentioned above, even passing "by reference" is just moving the value of a reference itself.

This is somewhat of an apples to oranges comparison. Rust has the same kind of indirection as C, it just calls it "references", not "pointers". The borrow checker is not a construct in one's Rust program, nor does it affect the runtime behavior of correct Rust programs, either. For example, there is an experimental Rust compiler written in C++ that is incomplete and lacks the borrow checker component. However, it can generate correct code just fine for proven-correct Rust programs.

I'm afraid I don't follow anymore. What is a "reference with a move capacity"? Again, pass by value and move are synonimous in Rust. A C variable is like a by-value binding in Rust. When we are not being super precise, we just call by-value bindings "variables" in Rust, too.

Copy is again, only a compile-time marker, which shows that "this type is allowed to be duplicated without special constructor code being run, simply by slavishly copying it bit-by-bit, and without invalidating its previous location when it is moved". This is how pretty much every type behaves in C. (Except for weird second-class "types" like arrays that don't even know how to copy themselves bit-by-bit, but that's just C shenaningans, nothing deep.)

That's a correct observation indeed.

Move is definitely pass by value. From this part on, I am lost as to what you mean by "owning refs" – maybe again you mean by-value bindings; however, if not, then I feel I might be trying to run in circles while trying to explain something that we view in a fundamentally different way.

1 Like

To clarify, what I mean by owned reference:

let owned_reference = vec![3,4,5];
println!("{:?}", owned_reference); // moved

The owned_reference behaves more like a pointer than a C ref. I base this mostly on the fact that I can dereference the above owned_reference, but cannot do the same in C.

int i = 3; 
int j = *i; /* cannot deref i */
int *p = &i; 
int j = *p; /* ok */ 

I think I understand why this was a confusing description. But first,

I think I figured out a way to align what I'm trying to convey. If in that statement, you mean to convey that Rust and C accomplish the "pass by value" by the same "bit by bit" copying then hear me out.

Another way to define the "call by value" capacity, is describing it as a means to limit/control the scope of what the function mutates. Said differently, call by value enables sending a value to a function without having to worry about the value itself being mutated. This is a valuable quality. In particular, in contrast to the pain of Fortran and Pascal. The problem is, bit by bit copying is expensive if sometimes impossible. Thus, the motivation for using pointers.

I think what has taken Rust so long to "come to be" is the time it's taken to specify a more nuanced and useful understanding of the problem the C "pass by value" was trying to solve. That and we will never be able to copy things fast enough; demand for speed and content will always outstrip the supply that copy can deliver. So, call-by-value:

is about limiting the scope of what can be mutated

If copying were free, there would be no problem (mostly anyway). However, copying is like being pregnant - there is no in-between. So the only way to even reduce the cost of copying, is to not copy at all. At best, a toggle. So, call-by-value:

is about avoiding having to copy values to solve the problem

The nuance has to be in a better understanding of "what has to be" to share memory. We know the issues with sharing memory; "double free", surprise mutations including the dreaded null pointer etc...

is about orchestrating access (what and when); likely realizing that simultaneous mutation capacity of the very same bit is not what anyone ever needs; sequential access is.

And thus the gateway into ownership to ensure a single drop event, the borrow checker to orchestrate/effect a tight accounting of as many reads as we want except when... And even better, do all of this accounting at compile time.

Lot's of conjecture here. Rust seems to accomplish the task of limiting what is mutated, not by copying, but by passing around references that bestow an orchestrated set of privileges ... If what is meant by

is: "in order to limit the scope of what a function mutates", that helps. If we further extended the statement to include something like "but unlike the pass by value implementation in C, Rust accomplishes the task without resorting to copying nor the use of pointers as you know them in C and C++", then we have it.

My key take-away would be what it means more precisely about "not resorting to pointers". In what I've come to take-away is that they are very much more like pointers and not a C variable used when "passing by value". Further, I would conclude the three ways to access a value in Rust are mostly one in the same; they are all similar to a pointer in C (they all have the memory address, they all have a dereferencing capacity, and include memory size and when appropriate, length built-in to the reference). They differ in what privileges are conferred with that access. Move is a bit of a misnomer only to the degree it conjures a unique capacity for changing the location of memory when in fact, it really only has the unique capacity to trigger the call to free() (move it to the trash) when it is out of scope. References with permission to mutate, invalidate previous references except the single reference with move privileges. Only a reference with move privileges can invalidate another reference with move privileges (a natural inference required for delivering on the promise to be the last, and only standing for that memory)... Something like that anyway.

Thanks for your pointed observations and push back to help augment my thought process here.

Your owned_reference is not a reference by any means. It's a value of type Vec<i32>.

The only reason you can dereference it is because Vec<T> implements Deref<Target=[T]> so that you can implicitly pass a reference-to-Vec where a reference-to-slice is expected, purely for reasons of convenience. You can read more about deref coercions here.

You can't dereference non-Deref types in Rust, either. If you write

let i: i32 = 3;
let j: i32 = *i;

then this does not compile, either. If you want to see this with a non-primitive type, make yourself a struct Foo; and retry, it won't compile, either.

Furthermore, C doesn't have the terminology "reference". C has pointers. Rust has references, and Rust references are pretty much the same as C pointers, except the compiler is smarter. (Rust also has "raw" pointers called… surprise, "raw pointers", which are almost a 1:1 mapping of C pointers, with a few differences in details such as what pointer types can be cast to one another.)

While it is true that some references (&T) do not allow mutation through themselves (mostly), and some references (&mut T) do allow mutation through themselves, it is not true that "Rust seems to accomplish the task of limiting what is mutated, not by copying". If you pass around a value, you don't implicitly just pass around a reference to it. Copying does happen. But then again, all of this is not really about mutation. The single-ownership model is primarily useful for preventing double-free of resources, regardless of whether or not they can be mutated.

Meanwhile, the RWLock pattern, i.e. the fact that you are only allowed to mutate through an exclusive reference, i.e. "when noone else is looking," is useful for avoiding problems similar to iterator invalidation. These are distinct classes of problems with distinct solutions. Ownership is not about mutation, RWLock is not about ownership, and neither of them is in itself sufficient for preventing memory unsafety. They prevent disjoint subsets of memory unsafety, and both of them are needed to work together.

Again, this is not true, Rust absolutely does perform copying when passing by value. This may or may not be optimized out later, but it usually is, so ultimately it does not matter.

Move is not the same as ownership. Move is the act of transferring ownership. Ownership is the "right to destroy" instead. Furthermore, moving does involve putting bits from one location to another. For example, see this playground. However, since you can't "take away" or "steal" bits from a region of memory, this conceptual "move" is realized by copying. However, in order to uphold the promise of not copying, only "moving", the compiler considers the original location to be uninitialized, "garbage", after the move.

4 Likes

You really know your stuff. I was definitely confused about what was going on with *vec_value. I know and really appreciate the Deref trait. I have to read through in more detail what you said and figure out when and how to pass by value in Rust. If a move is about ownership, then maybe when the compiler screams can’t move because no Copy is precisely when Rust is trying to perform “pass by value” but cannot until I implement the requested trait. If correct, this is a big “aha” for me. Big.

2 Likes

That's conceptually right, but that particular error message is often misleading. Usually, the correct fix isn't to implement Copy. Because Copy has special meaning to the compiler, It's only possible to implement it for types that are safe to duplicate without bespoke code. Lots of types don't fall into that category, particularly those that manage heap memory (Vec, Rc, etc.). Instead, you usually want to either clone() the value or rearrange your code so that the operation that takes ownership occurs last.

3 Likes

It’s worth pointing out that a move vs copy is the same operation at the machine level. It’s Rust that attaches extra semantics to them. Namely, when moving src to dest, src is marked as unusable (invalid); a copy is the same operation except src remains valid. Optimizing compilers can elide the actual machine (assembly) level data shuffling but that’s implementation/optimization details.

“pass by value” is a slightly overloaded term, people tend to use it to mean different things. In Rust it’s best to distinguish movement of data as either a move or a copy - that conveys the semantics more clearly. Reason is because passing a &T around is also a type of “pass by value” since the reference is being copied around and “passed by value” (where the “value” is the ptr address).

4 Likes

The concepts of "ownership" and "move" are separate.

Which would mean the following statement bestows ownership functionality to value_owner:

let value_owner = FooWithNoCopyTrait { /* .... */ }

The underlying encoding of that ownership capacity can be seen in its type

show_type(value_owner)
>> FooWithNoCopyTrait

Given the above,

// copy | move | both?
let something = value_owner;
println!("If the type is the same, the bits encode the instance.");
show_type(&something); // prints the T in &T
println!("If the mem address is different, new memory was allocated.");
println!("Where the value is stored: {:p}", &something);
// and is "movable"
println!("Movable: {}", move_it(something));

Playground

New memory is created to create a value of the same type, "walks like a duck" -> a copy.

A copy also implies I now have... a copy :)) i.e., a new separate instance, two instances, the original and the copy. If this is true, and given Rust has already chosen to "spend the resources" to make the copy, the issue of sharing should not be a problem. And with it, separate ownership and capacity to share etc. There are two separate instances after all.

However, as I know and love, if I try to read the value_owned following the move, I get the E0382 error that indicates "used after its contents were moved elsewhere". Similarly, I will get the E0505 error if I try to read a reference created prior to the move ("moved out while still borrowed").

Clearly, Rust won't let me access the original value. So all in all, move yes, copy "yes but".

It seems like a lot of effort to only bestow the right to destroy to another scope:

So, if I pass ownership on to another scope, I can align when the memory will be freed with that of when the instance is no longer useful to the computing task at hand. But, to do so, I have to create one copy and invalidate another to do so. Wow. A move, or whatever version of "pass by value" appears to be going on here, looks unappealing. The larger the benefit of adjusting the time when the memory can be free'd, the larger the cost associated with the copy. Little bang for the buck

Should I choose to implement Copy (which requires Clone) it gives me more of what I would expect from a "pass by value", in that at least I get two copies of accessible values.

Let me backup for a second, how did Rust make a copy in the first place? specifically without my implementing the Copy trait? I did not implement Clone. The copies were made, I just could not access one of them!

I believe the answer lies in the scope of the memory we are talking about. Perhaps it's something attune to a shallow-copy. Only one "shallow-copy" of the data can access the full version of the memory. Only that one "shallow-copy" is responsible for freeing the memory when it is out of scope.

As @vitalyd mentioned, the inaccessible memory hangs around, but that it is tagged as "unusable". Why not free it right away?... Accounting 101. The ledger has both a deposit and a withdrawal column. If this is "a zero sum game", then both pluses and minuses need to be tracked. How else might the compiler know which references are valid?.. pointing to free'd memory? Not.

When I was first introduced to the concept ownership, Niko Matsakis described something that I was calling a reference with ownership privileges to elements within our data (the confusing term). Based on all that has been discussed, what Niko was describing is perhaps the shallow copy I'm now trying to use. In JS a shallow copy is a collection of pointers to elements in my Objects; that's consistent. The term shallow copy for what's going on in Rust does not capture its capacity. Perhaps a better term might be "gateway" = shallow copy + toggle whether it is the current owner (i.e., is valid). So it's something like

 enum Gateway { Valid(ShallowCopy) | Invalid(ShallowCopy) }

Note, earlier in the post, I analogized that copying was like being pregnant; there is not "in-between". Shallow-copying is clearly a well-used pattern that negates that analogy .

This shallow copy is something Rust can create without input from me. Rust inherently would have to know how to do so for anything that qualifies as a "shallow copy"; likely a collection of references. Given that it knew how to "all along", there has to be a reason Rust won't just bake it into the runtime unless the user has signaled to do so.

Rust "had to know" all along, because the copy was made in both scenarios, regardless of my implementing the Copy trait. In the first scenario, Rust was making copies of the memory for the sole purpose of managing which scope owns the responsibility for freeing the memory. The only way it all works, is if Rust knows how to make "shallow copies" with the required accounting, all on its own. In the playground (above), it's clear rust ignores my implementation of Clone at runtime (presumably in lieu of its own). So a missing implementation detail is not the limiting factor to making two independent versions of the memory.

Manually calling clone, the "deep copy" and types that implement Drop

Somehow I think they are related. In the Playground sample I provided, Rust likely ignored my implementation of Clone at runtime because I wasn't "adding value"...it had a default path that is clearly sufficient for both scenarios.

Perhaps this is a case where the shallow copy = the deep copy. The only reason I was able to circumvented the compile-time move errors (E0382 and E0505) was because I signaled/marked it was ok to make the shallow copy required to enable two valid gateways, valid because they each have their own "deep-copy". If in this case, the shallow and deep-copy are "one in the same", there are likely dynamics that remain to be described.

I have not found a structure that "should" be something that can be replicated, that can't be derived by Rust. I can't think of a dominant reason why they shouldn't exist. Vec and String fall into a different category. They have at least one raw pointer, likely an immediate "no-go" from the borrow-checker. They both implement the Drop traits. Way back, in a New Rustacean, Chris Krycho's podcast, I recall him describing the reciprocal (if you will) occurrence of Drop and Copy (...but not Clone). Drop and Copy don't coexist very well; it's not a rule, but there are clearly overlapping qualities of the instances that do/don't implement the traits that make it more likely.

Playground - Manual cloning: yes, automagic: no.

So Rust has the know-how to replicate a Vec and String but, for some reason blocks me from exploiting the automagic that comes with Copy. I have to manually call a function that uses the Clone trait. The only difference is the presence of a marker trait. Is it just that Rust is encouraging me to "think different"ly about the decision to "pass by value" with these inherently larger collections? If this idea of "shallow" vs "deep" copy is true, is it a line between the stack and heap?

To the original question:
A move semantic involves at minimum, a "shallow-copy" of the value being moved. In the event, the Copy marker trait is implemented for that type, a second owner-instance is created. In contrast to when the Copy marker trait is absent, the second instance does not invalidate anything regarding the original. Unless there is something meaningful in the difference between these copies (shallow vs deep), the incremental cost, before optimization, seems unknowable. While mostly as described 2,000 words before, it's seems like the move is more expensive than first presumed. If not the case, more reliance on the compiler to optimize the cost away anyway.

In general, Rust types are relocatable: their semantics are unrelated to their memory location. In other words, you’re free to move a Rust type around in memory all you want as long as there are no outside references to it.

This means the compiler can do whatever copying it wants, as long as it ensures that only one block of memory is considered to “be” the value at one time.

It is conceptually freed immediately, in that the compiler may choose to use it for other purposes. Often, though, this memory is in the middle of a function’s stack frame, and can’t be “deallocated” without also deallocating the other local variables of that function.


The underlying meaning of the Copy trait is that the bit-for-bit shallow copy you describe, which is always used for moves, is sufficient to duplicate the value. This is true for simple types like integers, but untrue for objects which manage heap memory— duplicating those would result in a double-free error when the two copies get dropped.

Because Copy both changes the user-facing behavior of a type and restricts how that type can be implemented, it’s never auto-derived: The type implementor needs to decide whether to let users have implicit copies or to reserve the wider design space for future versions.

2 Likes

It's literally just a bitwise copy. It's not a "lot of effort". And you don't need Copy for it. A type that does not implement Copy can still be copied bit-by-bit. Everything can be copied bit-by-bit, that's literally the only operation that you can do with any region of memory at all. Except that you are not allowed to do it at the language level, because that would result in problems and inconsistencies with the guarantees the language is trying to give you.

The bitwise copy conceptually happens at the low level, at the level of LLVM or machine code, it's only about raw memory. However, implementing or not implementing the Copy trait is a type system thing. It's only a marker. It doesn't affect what happens at runtime at all. It only serves as a flag for the compiler as to whether it needs to consider earlier copies invalid. If a type is not Copy, then a move means "raw bitwise copy and invalidate the original". If a type is Copy, then a move means "raw bitwise copy but leave the original accessible".

It's like copyright in the real world. Can you get a copy of your favorite singer's album by simply downloading it from The Pirate Bay? Sure. It's physically possible. But it's most likely illegal. Except if they specifically released it with the permission to distribute freely, in which case any copying you are physcially able to perform is also legal. What's possible and what's legal is just not the same thing in general.

You are confusing the act of freeing the memory and the act of marking it as unusable.

First of all, you can't "free it right away". Most often, moved values are on the stack, which you can't just free in small pieces. The stack is grown and shrunk in blocks. Every function has its own necessary stack space (called a "stack frame") pre-calculated by the compiler, and a function call reserves that much memory, then a subsequent return from that function frees that memory. In between, there's no allocation or freeing going on at all. (Unless you do devious things like alloca, but that's not a thing in Rust.)

It's also possible to move from a block of heap-allocated objects. For example, Vec::into_iterator() is implemented in this manner. As you iterate over the contents of the vector, it moves the items out of its heap buffer one-by-one, returning them. Since the buffer of the vector is one contiguous allocation (that's pretty much the definition of a vector), it's not possible to free the moved-from originals in the buffer right away. The iterator has to wait until either all the elements have been consumed, or the iterator itself is dropped. Only then can it free the whole buffer, with all – now invalidated – elements in it.

Now, the "marking the original as unusable" business that I was talking about in my previous posts is a purely compile-time thing. It has nothing to do with freeing memory. It merely says to the compiler at compile time: "I made this new variable Y by moving the value from variable X". The compiler does two things when encountering a move in the source code:

  1. It emits a memcpy(&y, &x, size_of::<T>()) call in the resulting machine code
  2. Unless T: Copy, it registers "x is not allowed to be used anymore" in its own internal representation. This does not generate any code. "Valid" and "invalid" are not a thing at runtime. There's no such thing as "invalid memory" at runtime. It's merely a semantic construct that the compiler enforces in order to prevent you from shooting yourself in the foot. (Well, if you are writing deviously un-idiomatic code, there can be cases when it can't do this bookkeeping purely at compile-time, but for actually making the distinction, this mental model is useful. For the full truth, you can read about drop flags.)

In Rust, this shallow copy is just the literal duplication of the original value bit-by-bit. There are no pointers going on.

Perhaps this is better terminology. However, again, there's no such enum unless something funny (such as conditional initialization) is happening. Validity and invalidity is already determined when the compiler analyzes your code.

Again, I'm not sure why you bring up "referenes" here, because this issue has nothing to do with references. Apart from that, of course Rust knows inherently how to copy anything, because anything can be copied by reading its bits and writing them to another place. There's no magic in it.

Clone is not magic, either. It's not special to the compiler. Clone::clone() is only called when you run it manually. It won't ever be invoked by the compiler automatically for the purposes of moving. Moving is always a raw bitwise copy, nothing else.

In this regard, Clone and Copy are completely independent, technically. However, for reasons of good practice, Copy has the Clone supertrait because being able to duplicate automatically (existing Copy impl) but not being able to duplicate manually (no Clone impl) would be weird.

Furthermore, you are expected to implement Clone for Copy types in a way that their result is the same, but it is not enforced. I.e. the implementation of Clone::clone() is expected to be fn clone(&self) { *self } (or the equivalent, slightly more roundabout way that the #[derive] impl generates), but you can go ahead and ignore this, just like you did in your first playground.

This doesn't, however, change the fact that Clone::clone() won't be invoked automatically, ever. The compiler doesn't use it for creating bitwise copies, and the whole point of a Clone trait is to force you to use it manually in order to signal to the readers of the code that it might be more expensive than a bitwise copy.

No, it ignored the Clone impl because it always ignores the Clone impl, since moving is always bitwise copying and it is always trivial.

Oh, it absolutely is. A Copy type can't be Drop, and a Drop type can't be Copy, this just doesn't make sense, and it is enforced by the compiler.

The raw pointer indeed is the deep underlying reason of why Vec and String can't be Copy, but the absence or presence of a raw pointer field is not what the compiler is looking at, since raw pointers are Copy. It's first of all that due to their owning property, Copy is intentionally not implemented by the developers of the standard library for String and Vec. Furthermore, even if they wanted to, they couldn't, because they should implement Drop in order not to leak memory, and as per the above, the Drop impl makes Copy impossible.

Again, you are confusing Copy and Clone here. These two only do the same thing for trivial types like integers and structs containing only trivial types. Since Vec and String have underlying buffers, a high-level, semantic copy involves allocating a new heap buffer and copying the contents of the old heap buffer to the new heap buffer. This obviously doesn't invalidate the original vector or string. This whole mechanism is manually implemented in Vec::clone(), and the compiler doesn't "know" how to do it at all.

However, moving a vector or a string doesn't do any of that, it's still just a raw bitwise copy of the (pointer, length, capacity) triplet, without the allocation of a new, distinct heap buffer. This would lead to double-free errors if Vec or String were Copy.


At that point, I think I have said and written way too much already. I will not try to reiterate any of the above points further.

3 Likes

@H2CO3 Incredibly helpful. Greatly appreciated. The desire to reciprocate is a powerful human instinct (made clear when my kids gave us their drawings the minute they learned how to draw). I hope that how I articulated my path to a better understanding is a useful contribution to this valuable platform. Your input helped me, clearly, a bunch (... not a reference; I will not use that term in a move context because it’s not :)). Thank you, and to everyone who contributed.