Regarding the lifetime of heap allocations


#1

I have another question regarding this code:

This is one version (here just the main function):

fn main() {
    use std::fs::File;
    use std::io::Read;

    let mut data = String::new();
    File::open("points.json").unwrap().read_to_string(&mut data).unwrap();

    let points = data
                 .trim_matches(&['[', ']'][..])
                 .split("],[")
                 .map(|r| r.split(",").map(|p| p.parse().unwrap()))
                 .map(|mut r| V2(r.next().unwrap(), r.next().unwrap()))
                 .collect::<Vec<V2>>();

    let iterations = 300;
    println!("The average time is {:.1} ms.", benchmark(&points, iterations) * 1000.0);
}

And this is modified code:

fn load_data(file_name: &str) -> Vec<V2> {
    use std::fs::File;
    use std::io::Read;

    let mut data = String::new();
    File::open(file_name).unwrap().read_to_string(&mut data).unwrap();

    data
    .trim_matches(&['[', ']'][..])
    .split("],[")
    .map(|r| r.split(",").map(|p| p.parse().unwrap()))
    .map(|mut r| V2(r.next().unwrap(), r.next().unwrap()))
    .collect::<Vec<V2>>()
}

fn main() {
    let points = load_data("points.json");
    let iterations = 300;
    println!("The average time is {:.1} ms.", benchmark(&points, iterations) * 1000.0);
}

I’ve seen that the original version runs using about 9 MB RAM, while the modified takes about 5 MB. This is interesting. I guess the cause is that in the first version the data string is kept in memory until the end of the program, while in the second program it’s deallocated when load_data ends (the string is about 3.8 MB).

The situation in a simpler example:

fn main() {
    let v1 = vec![10, 20, 30];
    let x = v1[1];
    let mut v2 = vec![1, 2, 3, 4];
    v2[1] = x;
}

I guess currently v1 is kept allocated until the end of main(), but the vector v1 is never referenced past the assignment of x, and the Rust compiler tracks the lifetimes of all variables. So why isn’t Rust deallocating v1 after the creation of x?


#2

That’s about right. Local variables are deallocated when their scope ends unless they were deallocated before (and trim_matches takes self by reference, so it does not deallocate its input).

This is done to ensure predictability. If we did not deallocate at precise points, then random changes to your code structure could change where things are deallocated. Worse, if v1 had a real destructor, they could change when that destructor runs.