Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Rust free up the memory of overwritten variables?

I saw in the Rust book that you can define two different variables with the same name:

let hello = "Hello";
let hello = "Goodbye";

println!("My variable hello contains: {}", hello);

This prints out:

My variable hello contains: Goodbye

What happens with the first hello? Does it get freed up? How could I access it?

I know it would be bad to name two variables the same, but if this happens by accident because I declare it 100 lines below it could be a real pain.

like image 576
Alexander Luna Avatar asked Jan 12 '18 13:01

Alexander Luna


3 Answers

Rust does not have a garbage collector.

Does Rust free up the memory of overwritten variables?

Yes, otherwise it'd be a memory leak, which would be a pretty terrible design decision. The memory is freed when the variable is reassigned:

struct Noisy;
impl Drop for Noisy {
    fn drop(&mut self) {
        eprintln!("Dropped")
    }
}

fn main() {
    eprintln!("0");
    let mut thing = Noisy;
    eprintln!("1");
    thing = Noisy;
    eprintln!("2");
}
0
1
Dropped
2
Dropped

what happens with the first hello

It is shadowed.

Nothing "special" happens to the data referenced by the variable, other than the fact that you can no longer access it. It is still dropped when the variable goes out of scope:

struct Noisy;
impl Drop for Noisy {
    fn drop(&mut self) {
        eprintln!("Dropped")
    }
}

fn main() {
    eprintln!("0");
    let thing = Noisy;
    eprintln!("1");
    let thing = Noisy;
    eprintln!("2");
}
0
1
2
Dropped
Dropped

See also:

  • Is the resource of a shadowed variable binding freed immediately?

I know it would be bad to name two variables the same

It's not "bad", it's a design decision. I would say that using shadowing like this is a bad idea:

let x = "Anna";
println!("User's name is {}", x);
let x = 42;
println!("The tax rate is {}", x);

Using shadowing like this is reasonable to me:

let name = String::from("  Vivian ");
let name = name.trim();
println!("User's name is {}", name);

See also:

  • Why do I need rebinding/shadowing when I can have mutable variable binding?

but if this happens by accident because I declare it 100 lines below it could be a real pain.

Don't have functions that are so big that you "accidentally" do something. That's applicable in any programming language.

Is there a way of cleaning memory manually?

You can call drop:

eprintln!("0");
let thing = Noisy;
drop(thing);
eprintln!("1");
let thing = Noisy;
eprintln!("2");
0
Dropped
1
2
Dropped

However, as oli_obk - ker points out, the stack memory taken by the variable will not be freed until the function exits, only the resources taken by the variable.

All discussions of drop require showing its (very complicated) implementation:

fn drop<T>(_: T) {}

What if I declare the variable in a global scope outside of the other functions?

Global variables are never freed, if you can even create them to start with.

like image 121
Shepmaster Avatar answered Nov 17 '22 18:11

Shepmaster


There is a difference between shadowing and reassigning (overwriting) a variable when it comes to drop order.

All local variables are normally dropped when they go out of scope, in reverse order of declaration (see The Rust Programming Language's chapter on Drop). This includes shadowed variables. It's easy to check this by wrapping the value in a simple wrapper struct that prints something when it (the wrapper) is dropped (just before the value itself is dropped):

use std::fmt::Debug;

struct NoisyDrop<T: Debug>(T);

impl<T: Debug> Drop for NoisyDrop<T> {
    fn drop(&mut self) {
        println!("dropping {:?}", self.0);
    }
}

fn main() {
    let hello = NoisyDrop("Hello");
    let hello = NoisyDrop("Goodbye");

    println!("My variable hello contains: {}", hello.0);
}

prints the following (playground):

My variable hello contains: Goodbye
dropping "Goodbye"
dropping "Hello"

That's because a new let binding in a scope does not overwrite the previous binding, so it's just as if you had written

    let hello1 = NoisyDrop("Hello");
    let hello2 = NoisyDrop("Goodbye");

    println!("My variable hello contains: {}", hello2.0);

Notice that this behavior is different from the following, superficially very similar, code (playground):

fn main() {
    let mut hello = NoisyDrop("Hello");
    hello = NoisyDrop("Goodbye");

    println!("My variable hello contains: {}", hello.0);
}

which not only drops them in the opposite order, but drops the first value before printing the message! That's because when you assign to a variable (instead of shadowing it with a new one), the original value gets dropped first, before the new value is moved in.

I began by saying that local variables are "normally" dropped when they go out of scope. Because you can move values into and out of variables, the analysis of figuring out when variables need to be dropped can sometimes not be done until runtime. In such cases, the compiler actually inserts code to track "liveness" and drop those values when necessary, so you can't accidentally cause leaks by overwriting a value. (However, it's still possible to safely leak memory by calling mem::forget, or by creating an Rc-cycle with internal mutability.)

See also

  • What's the semantic of assignment in Rust?
like image 24
trent Avatar answered Nov 17 '22 20:11

trent


There are a few things to note here:

In the program you gave, when compiling it, the "Hello" string does not appear in the binary. This might be a compiler optimization because the first value is not used.

fn main(){
  let hello = "Hello xxxxxxxxxxxxxxxx"; // Added for searching more easily.
  let hello = "Goodbye";

  println!("My variable hello contains: {}", hello);
}

Then test:

$ rustc  ./stackoverflow.rs

$ cat stackoverflow | grep "xxx"
# No results

$ cat stackoverflow | grep "Goodbye"
Binary file (standard input) matches

$ cat stackoverflow | grep "My variable hello contains"
Binary file (standard input) matches

Note that if you print the first value, the string does appear in the binary though, so this proves that this is a compiler optimization to not store unused values.

Another thing to consider is that both values assigned to hello (i.e. "Hello" and "Goodbye") have a &str type. This is a pointer to a string stored statically in the binary after compiling. An example of a dynamically generated string would be when you generate a hash from some data, like MD5 or SHA algorithms (the resulting string does not exist statically in the binary).

fn main(){
  // Added the type to make it more clear.
  let hello: &str = "Hello";
  let hello: &str = "Goodbye";

  // This is wrong (does not compile):
  // let hello: String = "Goodbye";

  println!("My variable hello contains: {}", hello);
}

This means that the variable is simply pointing to a location in the static memory. No memory gets allocated during runtime, nor gets freed. Even if the optimization mentioned above didn't exist (i.e. omit unused strings), only the memory address location pointed by hello would change, but the memory is still used by static strings.

The story would be different for a String type, and for that refer to the other answers.

like image 1
Chris Vilches Avatar answered Nov 17 '22 19:11

Chris Vilches