Rust, unlike C and C++, has an automatic memory management system. It revolves around 3 main concepts in Rust:
- Ownership
- Borrowing
- Lifetimes
You must have come across malloc,calloc and realloc in C/C++. These methods were for memory allocation and deallocation during the lifecycle of the program. They had to be called manually.
In this article, we'll learn about manual memory management and and automatic memory management and then about ownership. Pre-requisite of this article is to understand how stack and heap memory works. We know that how a vector full of numbers is going to be stored on a heap memory.
We are aware of the fact that when a function returns a vector, it stores the metadata of the vector in the stack memory and it stores the actual elements of the vector in the heap memory.
Let's talk about the Heap's memory management system. Whenever we call a vec![] macro like below:
let nums = vec![1, 2, 3];Rust actuall calls alloc(3) internally to allocate 3 places in the heap memory for 1, 2 and 3 numbers of the vector. It takes non-trivial amount of time to run in order to allocate the memory for the three numbers of the vector. What it does is it finds 3 unused heap bytes in a row and marks them as in-use when it finishes running.
On a side note, alloc in C and C++ does tend to refer to stack or heap memory allocation but in our case we are referring to heap memory. If we are not specific about alloc, it usually refers to the heap memory. Often languages suggest developers to reduce the amount of alloc they do, i.e. the amount of memory-management in heap they do because these are expensive operations. Managing memory takes significant amount of time if the sequence of unused bytes required is large.

When alloc(3) is called, we try to find 3 consecutive bytes in the memory and mark them in-use. Now, our computer's hardware does not have an actual concept of a stack and a heap. It has only series of 0s and 1s. So when you run alloc() it's actually running a non-trivial amount of logic managing the stack and heap memory indices. This logic contains data-structures and algorithms to manage these huge stacks. It's actuall traversing these stacks to find out where the 3 unused bytes in a row are actually present. One more important question arises is how does memory management knows that these bytes are not in use. It is very obvious in the stack when the previous variables are not in use but in heap it is not obvious that when these 3 in-use bytes need to be marked as unused and made available for the other programs to store on it.
Whenever people talk about memory management, what they tend to mean is the question of when is it safe to mark something on the heap as no longer in use? Figuring out when it is safe to free up the memory is a lot more trickier than allocating the memory on the heap. Let's take a look at below code snippet:
fn get_final_orders() -> i64 {
let orders = vec![1, 2, 3, 4]; // alloc
let mut total_orders = 0;
for order in orders.iter() {
total_orders += order;
}
let final_orders = finish(total_orders);
return final_orders;
}
finish(total) {
// some logic here
return total;
}
get_final_orders();This code snippet is fairly simple. It has 2 methods, get_final_orders and finish. It declares an orders vector. It also declares total_orders which is mutable. It iterates over orders and sums all orders and stores in total_orders. Finally, we pass the computed total_orders into finish and return it back to finish_orders. Finally, we return final_orders from the function. For now, don't bother about finish method.
According to this code, our first memory/heap allocation — alloc operation shall happen when orders is allocated a vector of numbers. The question is where do we want to dealloc the memory from the heap via memory management system? Where do we actually say that these 4 slots of memory that we had requested for to store the orders vector is now safe to de-allocate and released back to the running program. We can say, never de-allocate any memory. But this creates a memory leak. Your program keeps running and never frees up the memory and the memory keeps going up until the program completely runs out of memory. So dealloc() is mandatory. It does not happen magically, so Rust needs to do that. In C/C++ you do have a function free() to de-allocate the memory.
So, if we were in C/C++ world, we would have called dealloc(orders) somewhere like shown below in the code snippet, below let final_orders:
fn get_final_orders() -> i64 {
let orders = vec![1, 2, 3, 4]; // alloc
let mut total_orders = 0;
for order in orders.iter() {
total_orders += order;
}
let final_orders = finish(total_orders);
dealloc(orders); // <---- DE-ALLOCATE HERE
return final_orders;
}
finish(total) {
// some logic here
return total;
}
get_final_orders();orders was last used in the iteration. What happens if we dealloc(orders) above the iteration, like shown in the below snippet:
fn get_final_orders() -> i64 {
let orders = vec![1, 2, 3, 4]; // alloc
let mut total_orders = 0;
dealloc(orders); // <---- DE-ALLOCATE HERE
for order in orders.iter() {
total_orders += order;
}
let final_orders = finish(total_orders);
return final_orders;
}
finish(total) {
// some logic here
return total;
}
get_final_orders();The above de-allocation looks fishy. What bad things could happen here? After dealloc(orders) in the above snippet is executed, the Rust Memory Management System, does mark the slots as available for use. Remember, we do not care to erase the memory but we just say that other programs are free to use these slots of memory for their use. So if other Rust program is running in a parallel thread, it simply goes and updates these slots of memory with some other numbers which are not relevant to the current program. So when orders is iterated, it does not get [1, 2, 3, 4] but it gets some random non-relevant numbers and our results are not matching the expectation. So, we cannot dealloc(orders) before it's last use in the program. This kind of error in Rust is known as use after free error. i.e we are using memory after it's being freed. These are nasty bugs. They are really really hard to track down. This is why developers generally don't prefer doing manual memory management.
What if we do dealloc(orders) twice? Like below:
fn get_final_orders() -> i64 {
let orders = vec![1, 2, 3, 4]; // alloc
let mut total_orders = 0;
for order in orders.iter() {
total_orders += order;
}
dealloc(orders); // <---- DE-ALLOCATE HERE
let final_orders = finish(total_orders);
dealloc(orders); // <---- AND DE-ALLOCATE HERE
return final_orders;
}
finish(total) {
// some logic here
return total;
}
get_final_orders();This can cause double free errors. What I mean is, we've freed the memory slots using first dealloc(orders) and then we have a finish method which uses the same slots and we free them again below finish using the second dealloc(orders).
Fortunately, Rust does not as us to do manual memory management. So we do not encounter use after free or double free errors. This all the things that we learnt is building up to how Rust decides when to free the memory. This is what separates Rust from other languages in term of performance.
A lot of languages, like JavaScript, would do. something called as Garbage Collection. It's a way to decide when to free up the memory. Memory management system for Garbage Collection does have a little bit of overhead. This causes programs to pause the execution till the Garbage collection's memory management system has figured out what things to free up and which to retain. This is known as GC_PAUSE. This can result into not a good user experience. Garbage collections are, in a way, convenient as we only care about allocations. But it does have pretty serious downsides of the performance which can in some cases lead to failures.
So how does Rust manage the memory? In all the previous programming examples in this article series that we have seen, Rust has never asked us to deallocate memory manually. We've used vec and we've used string but we never had to manage the memory. Rust automatically inserts dealloc(orders) during compile time smartly at an appropriate place just before the return statement because it would no longer be used. Refer the below snippet:
fn get_final_orders() -> i64 {
let orders = vec![1, 2, 3, 4]; // alloc
let mut total_orders = 0;
for order in orders.iter() {
total_orders += order;
}
let final_orders = finish(total_orders);
// dealloc(orders); // <---- INSERT DE-ALLOCATE HERE COMPILE TIME
return final_orders;
}
finish(total) {
// some logic here
return total;
}
get_final_orders();In C/C++, you would have done this manually but Rust does this for you and it inserts it in correct place.
There are some edge cases with deallocations here where the Ownership, Borrowing and Lifetimes come into picture. These concepts originated from Rust's potential simple system of where to insert the dealloc call which is whenever the variable goes out of scope. Orders is out of scope in between below 2 lines:
let final_orders = finish(total_orders);
return final_orders;
Rust is able to guarantee that there is no use after free and no double free. So, Rust could have chosen an earlier time to de-allocate the memory a bit before i.e. before let final_orders = finish(total_orders); because orders is not used on this line or lines after it. In order to achieve what we want, i.e. to de-allocate orders before let final_orders = finish(total_orders); we can simply rewrite our code like this:
fn get_final_orders() -> i64 {
let mut total_orders = 0;
{ // <--- INTRODUCED A SCOPE HERE
let orders = vec![1, 2, 3, 4]; // alloc
for order in orders.iter() {
total_orders += order;
}
//dealloc(orders)
} // <--- SCOPE ENDS HERE
let final_orders = finish(total_orders);
return final_orders;
}
finish(total) {
// some logic here
return total;
}
get_final_orders();Notice the additional {} being added to the function get_final_orders. These brackets {} are known as anonymous scopes. The orders is going out of scope before the end of {} the anonymous scope, Rust frees up the memory which can be used by the finish function. You can use this to get a similar level of control as you would get in C/C++ where you could write free() whenever you wanted to.
Ownership
Finally, we come to the topic of Ownership which is based on the foundations mentioned above. Let's take a look at below code snippet:
fn get_years() -> Vec<i32> {
let years = vec![2001, 2002, 2003, 2004]; // alloc()
// dealloc(years)
return years;
}
fn main() {
let years = get_years();
}Notice where the alloc() and dealloc(years) are getting called. years memory slot is freed up as soon as the get_years() method is over. But the line inside main() still needs access to years value in the memory which contained [2001, 2002, 2003, 2004]. So now, we encounter the use after free bug. This is because years has bee de-allocated and yet we still reference the same memory. Fortunately, rust handles this edge case like a smooth operator! This is first of the several edge cases that we'll talk about it.
Rust says that current scope we are in while execution of get_years(), it owns the scope of the years. When Rust sees that this method was called by another method, in our case main(), it says transfer the ownership of memory management for years to the calling/parent function, i.e. main() in our case. Ownership essentially refers to — whose responsibility is it to deallocate years? Whenever the owner exits the scope, either it needs to transfer the ownership to somebody else OR if there is no one to transfer the ownership to, then de-allocate that memory. So, our above code snippet would look like below:
fn get_years() -> Vec<i32> {
let years = vec![2001, 2002, 2003, 2004]; // alloc()
return years; // transfer the ownership of years to main()
}
fn main() {
let years = get_years();
}Verbally in rust, it is known as moving a.k.a transfer of ownership. So verbally we say, we are going to move years from get_years() to main(). In the main() function, when the year goes out if scope, dealloc(years) is called. This avoids use after free bug.
Let us take an example of another code snippet:
fn print_years(years: Vec<i32>) {
// alloc
for year in years.iter() {
println!("{}", year);
}
// dealloc(years)
}
fn main() {
let years = vec![2001, 2002, 2003, 2004]; // alloc()
print_years(years); // transfer the ownership of years to print_years()
}So in the above case, we transfer the ownership of memory management of years from main() to print_years().
But what if we call print_years(years) twice?
fn print_years(years: Vec<i32>) {
// alloc
for year in years.iter() {
println!("{}", year);
}
// dealloc(years)
}
fn main() {
let years = vec![2001, 2002, 2003, 2004]; // alloc()
print_years(years); // years is deallocated after this call
print_years(years); // use after free bug
}years got de-allocated after the first call of print_years(). When we call it again, Rust throws us an use after free bug. Exact error you'll see is use of moved value years. User after move is a borrow checker error which is not present in any other language. How can we convince Rust not to free up the memory for years after the first call to print_years? We can return the value from print_years like below code snippet:
fn print_years(years: Vec<i32>) {
for year in years.iter() {
println!("{}", year);
}
return years;
}
fn main() {
let years = vec![2001, 2002, 2003, 2004]; // alloc()
let years2 = print_years(years); // get the ownership of returned value in years2
let years3 = print_years(years2);// get the ownership of returned value in years3
}We now get rid of use after move error. You are intentionally transferring the ownership to the caller of the function for it to management the memory of the value.
Another alternative is to use .clone() but we'd be consuming more memory in this case. See the example below:
fn print_years(years: Vec<i32>) {
for year in years.iter() {
println!("{}", year);
}
}
fn main() {
let years = vec![2001, 2002, 2003, 2004]; // alloc()
print_years(years.clone()); // transfer the ownership of years to print_years()
print_years(years2); // transfer the ownership of years to print_years()
}I hope you enjoyed this section about Ownership and Memory Management in Rust.
You can subscribe to my newsletter about The Rust Programming Language here. You can read about all the articles in this series here.
Rustaceans 🚀
Thank you for being a part of the Rustaceans community! Before you go:
- Show your appreciation with a clap and follow the publication
- Discover how you can contribute your own insights to Rustaceans.
- Connect with us: X | Weekly Rust Newsletter