Stack memory with move semantics


#1

Reading through the ‘book’, in section 4.1 it begins talking about move semantics, and how it works with memory on the stack vs memory on the heap. Using type String as an example I noticed that there is some minor overhead involved in how strings work. Mostly concerning length and capacity.

So if I were to create a new string. I would have pushed a new string on the stack with the following members:
https://doc.rust-lang.org/book/2018-edition/img/trpl04-01.svg

What this implies to me is that ptr, len, and capacity all live on the stack. So if I were to move that string into a new variable (shallow copy) then would that memory for ptr, len, and capacity still exist on the stack in the original variable and now also the new variable. Basically would I lose the ability to use that memory space in the original variable until it falls out of scope?

So if I were to pass this string into a function with the intent of moving it (not barrow) and not using it any longer, would it still exist in the original function but not be accessible?

I’m just interested because I haven’t seen anyone ask this, but I don’t want to blow up my available stack unknowingly because I didn’t understand what is going on behind the scenes.

Lets not consider optimizations at the moment.


#2

Rust currently allocates on to the stack at the beginning of a function call and deallocates stack memory at the end of a function call. So no (ignoring optimizations), you won’t be able to reuse stack memory manually To reuse the vaiable’s memory, simply reuse the variable. That said, you most likely won’t blow up the stack from 24 bytes of data (for String).

When you move a value from one variable to another, you completely lose access to the old variable binding until you reassign a value to the old variable binding. You will never get it back, but the memory is still allocated on the stack and won’t be freed until the function call ends. There are only 2 exceptions to this rule, if you’re type implements Copy, then you can always reuse the old variable binding. Note: moving a Copy type generates the same code before optimizations as moving non-Copy types. The second exception is &mut T, which is special cased to reborrow instead of move. This is to make the following code work.

let mut x = "Hello World".to_string();

let mut_x = &mut x;

drop(mut_x);
drop(mut_x);

desugars to

let mut x = "Hello World".to_string();

let mut_x = &mut x;

drop(&mut *mut_x);
drop(&mut *mut_x);

Another thing to note on Debug mode is that the memory allocated for a function is dependent on the number of variables you have, and is the sum of the size of all of the types of each variable. When you move a value between variables, the value is literally moving from 1 stack location to another. (This can be seen in the playground if you make it show you the asm).


If you want to see the asm of your project, you can use this cargo command

cargo rustc -- --emit asm
cargo rustc --release -- --emit asm

and the asm will be in these files (where crate_name is the name of your crate)

target/debug/deps/crate_name.s
target/release/deps/crate_name.s

Edit because if oversights mentioned in @Riateche’s reply


#3

You can assign a new value to a variable after moving out the previous value:

let mut a = some_string;
let b = a;
a = some_other_string;

#4

Unoptimized codegen is pretty bad in terms of stack frame usage, register allocation, and pretty much anything else efficiency-related (unsurprisingly). If you take code like the following:

fn foo() {
    let mut s = "hello".to_string();
    s = "world".to_string();
    s = "again".to_string();
    s = "and again".to_string();
}

and vary the number of reassignments, you’ll see the compiler (in Debug builds) varying the call frame size naively. For example, the above reserves 120 bytes of stack space:

playground::foo:
	subq	$120, %rsp

If you remove the last assignment, you get:

playground::foo:
	subq	$88, %rsp

You can observe a similar thing even with Copy types:

fn foo() {
    let mut x = [1;8];
    x = [2;8];
    x = [3;8];
    // vary how many statements like that, and stack size grows
    // accordingly
}

What’s interesting though is if you use scalar values or tuples, the stack slots are reused even in debug mode.


#5

Thanks for the information everyone. Pretty much exactly what I anticipated, but I didn’t want to just guess. It looks like I need to remain conscious of how I’m using types after binding them more than I initially thought.

Vitalyd,
Your answer is particularly interesting to me, and not what I would have guessed. I have to assume additional use of memory is necessary for debugging purpose though.

KrishnaSannasi,
One of the hard things for me at the moment getting my head into, is how Rust seems to disambiguate what a pointer is from the struct itself. I was working with c/c++ for a little bit of time, and those languages leave the pointers more malleable IMO. I’m gonna have to do a little more homework to fully understand what you laid out even in its simplicity. I mention pointers specifically, because I feel the use of the (&) operator and (*) operator leaves a little confusion. Based on how borrowing works, variable ownership, dereferencing, and such things.

I admit I really need to work on learning assembly. Just not quite there yet.


#6

Debug (unoptimized) builds are generally like that for, primarily, the following reasons:

  • faster compilation time (opto passes don’t run)
  • better debuggability, yeah

So stack usage will be higher due to compiler mostly going verbatim with your source code - that includes stack slot usage and lack of function inlining, which also contributes to more stack.

In general, Rust debug builds are very slow in runtime performance, produce bloated binaries, etc - all the “zero cost abstractions” stuff is essentially missing :slight_smile:.

What do you mean exactly?


#7

Its not a big deal vitalyd, and off topic from what my original question was. It seems that the two operators I had mentioned are exactly like c/c++. Being & and *, but it seems the book is trying to put a separation in mindset on how they should be viewed. Am I wrong about that?

https://doc.rust-lang.org/book/appendix-02-operators.html

Granted I really haven’t read anything about lifetimes yet. You don’t need to answer my question. Just reading through the book and figuring things out.

Its nice to have good response though. I really appreciate it.


#8

References (or borrows) in Rust can be looked at as “managed” (or checked, verified, what have you) pointers. By managed, I mean they come with some rules, and compiler enforces them. The rules are there to avoid memory unsafety, at compile time. At the codegen level, they’re no different than pointers/references in C++. When people ascribe more semantics to Rust’s references, as compared to C++, they’re talking about those (checked) rules they come with. Lifetime parameters are a way to relate different references in an abstract manner that the compiler can then use to verify (they’re like generic type parameters in a lot of ways).

Go through the book - it ought to help paint a fuller picture. And don’t be shy about posting questions to this forum.


#9

Rust references are not the same as C/C++ pointers. I find it useful to think of references as shared-read-locks (&) and unsharable-write-locks (&mut). If you do want to think of them as pointers, think of them as virtual pointers that the compiler will often optimize away (except in debug mode).


#10

One of the things that C++ ppl need to unlearn is that “references pretend to be values”. Generally that’s not true in Rust - they are separate types and you need the * and &.


#11

This was probably more apparent in early versions of Rust when there weren’t so many predefined coercions, thus requiring more explicit uses of * and &,
[Disclaimer] That was before I began to study Rust.