Navigation
Search
|
Rust memory management explained
Wednesday February 12, 2025. 10:00 AM , from InfoWorld
Rust shares many concepts with other languages intended for systems programming. For instance, Rust distinguishes between memory allocated from the stack and memory allocated from the heap. It also ensures that variables declared within a scope are unavailable outside of that scope.
But the Rust programming language implements these behaviors in its own way, with significant implications for how memory management works. Rust uses a variety of ownership metaphors to describe how resources are created, retained, and disposed of during a program’s lifetime. Learning how to work with Rust’s concept of ownership is an important rite of initiation for Rust programmers. This quick guide will help you get started. How Rust manages scope In C++ and other languages, there’s a rule called RAII: Resource Acquisition Is Initialization. The resources for a given thing (like memory) are tied to how long it lives in a program. Rust also employs this rule, which prevents resources from being freed more than once, or used after being deallocated. In Rust, as in other languages, objects all have a scope, such as the body of a function or a manually declared scope. An object’s scope is the duration for which it’s considered valid. Outside of that scope, the object doesn’t exist—it can’t be referenced, and its memory is automatically disposed of. Anything declared inside a given scope only “lives” as long as that scope does. In the following example, data will live throughout main(), including in the inner scope where other_data is declared. But other_data is only available in the smaller scope: fn main() { let mut data = 1; { data = 3; let mut other_data = 2; } other_data=4; } Compiling this code generates the error: cannot find value `other_data` in this scope on the next-to-last line. When something falls out of scope, it is not only inaccessible but its memory is automatically disposed of. What’s more, the compiler tracks the availability of a given thing through the course of a program, so attempting to access something after it has fallen out of scope triggers a compiler error. The previous example uses stack-allocated variables for all its variables, which are fixed in size. However, we can use the Box type to get heap-allocated variables, where the size may vary and there is more flexibility of use. fn main() { let mut data = Box::new(1); { data = 3; let mut other_data = Box::new(2); } other_data=4; } This code also won’t compile at first, for the same reasons. But if we modify it slightly, it will: fn main() { let mut data = Box::new(1); { data = 3; let mut other_data = Box::new(2); } } When this code runs, other_data will be heap-allocated inside the scope, and then automatically de-allocated when it leaves. The same goes for data: it’ll be created inside the scope of the main() function, and automatically disposed of when main() ends. All this is visible to the compiler at compile time, so mistakes involving scope don’t compile. Ownership in Rust Rust adds another key idea to scoping and RAII: the notion of ownership. Objects can only have one owner, or live reference, at a time. You can move the ownership of an object between variables, but you can’t refer to a given object mutably in more than one place at a time. fn main() { let a = Box::new(5); let _b = a; drop(a); } In this example, we create the value in a with a heap allocation, then assign _b to a. By doing this, we’ve moved the value out of a. So, if we try to manually deallocate the value with drop(), we get an error: use of moved value: `a. Change the last line to drop(_b), though, and everything is fine. In this case, we’re manipulating that value by way of its current, valid owner. A function call can also take ownership of what’s passed to it. Consider the following, adapted slightly from Rust By Example: fn bye(v:Box){ println!('{} is not being returned', v); } fn main() { let a = Box::new(5); let _b = a; bye(_b); drop(_b); } If we try to compile this code, we’ll get the error use of moved value: `_b at the line drop(_b). When we call bye(), the variable passed into it gets owned by bye(). And since that function never returns a value, that effectively ends _b‘s lifetime and deallocates it. The drop() would have no effect even if it were called. On the other hand, this would work: fn bye(v:Box)-> Box{ println!('{} is being returned', v); return v; } fn main() { let a = Box::new(5); let _b = a; let mut c = bye(_b); c = 32; drop(c); } Here, we return a value from bye(), which is received into an entirely new owner, c. We can then do as we like with c, including manually deallocating it. Something else we can do when we change owners is alter the mutability rules. An immutable object can be made mutable and vice versa: fn bye(v:Box)-> Box{ println!('{} is being returned', v); return v; } fn main() { let a = Box::new(5); let _b = a; let mut c = bye(_b); *c = 32; drop(c); } In this example, a and _b are both immutable. But c is mutable, and once it takes ownership of what a and _b referred to, we can reassign it (although we need to refer to it as *c to indicate the value contained in the Box). It’s important to note that Rust enforces all these rules ahead of time. If your code doesn’t honor how borrowing and scope work, it simply won’t compile. This ensures whole classes of memory bugs never make it to production. It just requires being much more scrupulous about what gets used where. Automatic memory management and Rust types I’ve mentioned using the Box type to heap-allocate memory and automatically dispose of it when it goes out of scope. Rust has a few other types that can be used to automatically manage memory in different scenarios. An Rc, or “reference counted” object, keeps track of how many clones are made of the given object. When a new clone is made, the reference count goes up by 1; when a clone goes out of its scope, the reference count drops by 1. When the reference count reaches zero, the Rc object is dropped. Likewise, the Arc type (atomic reference count) allows for this same behavior but across threads. A key difference between Rc/Arc and Box is that a Box lets you take exclusive ownership of something and make changes to it, while Rc and Arc share ownership, so it can only be read-only. The Rc/Arc objects follow the same rules as Boxes: they’re bound by Rust’s larger dictates around lifetimes and borrowing. You can’t use them to perform an end-run around those checks. Do use them when the structure of a program makes it hard to tell how many readers will exist for a given piece of data. One more type used for mutable memory management is RefCell. This type lets you also have a single mutable reference or multiple immutable references, but the rules about such use are enforced at runtime, not compile time. However, RefCell has two strong rules: it can only be used in single-threaded code, and if you break a RefCell‘s borrowing rules at runtime, the program will panic. To that end, it only works for a narrow range of problems that are exclusively solved at runtime. Rust memory management vs. garbage collection Even though Rust has types that allow reference counting, a mechanism also used in garbage-collected, memory-managed languages like Java, C#, and Python, Rust is generally not thought of as a “garbage-collected” or “memory-managed” language. Memory management in Rust is planned deterministically at compile time, rather than handled at runtime. Even reference counted types have to obey Rust’s rules for object lifetimes, scoping, and ownership—all of which must be confirmed at compile time. Also, languages with runtime memory management generally don’t offer direct control of when or how memory is allocated and reclaimed. They may give you some high-level knobs to tune, but you don’t get the granular control you do in Rust (or C, or C++). It’s a tradeoff: Rust requires more work ahead of time to ensure every use of memory is accounted for, but it pays off at runtime with faster execution and more predictable and reliable memory management.
https://www.infoworld.com/article/3815535/rust-memory-management-explained.html
Related News |
25 sources
Current Date
Feb, Thu 13 - 08:05 CET
|