In Rust, there are several ways to manage memory. Memory has two regions: One is called the stack. Each new scope (between { and }) adds variables to the stack. Variables on the stack are allocated at once when a new scope is introduced, and they are freed when the scope ends. This brings speed. Variables on the stack are also allocated contiguously, so they are more compatible with the principle of locality.
{
var1
var2
{
var3
var4
}
}
The inner scope variables can’t be accessed by the outer scope, but outer scope variables can be accessed by the inner scope. When we are in the inner scope, the stack contains both scopes’ variables. Stack-based memory management can be called table d’hôte. It’s faster and simpler.
However, in order to allocate them at once, variable sizes must be known beforehand. If the compiler doesn’t know the size of an array, it can’t allocate the array on the stack. We need another method to use these indefinite variables.
The other way of obtaining memory is using the heap. The heap is independent of the scope. You can allocate and fill it when you need with a size determined at runtime, share it with other scopes, and free it à la carte. Rust provides several different ways to manage memory on the heap.
In my experience, making the program flow as scoped as possible without cross-cutting memory management is a good sign. Ideally, a program should have a tree-like flow of execution, and the number of heap allocations is minimized in this case. However, most modern software systems are not designed this way, and memory management is not considered during the requirements phase. When you leave this to a garbage collector, even if it does its best, memory management often remains haphazard.
Ideally, when you run the program, it should call a series of functions that branch into further sub-functions:
flowchart LR
main--->fn1
main--->fn2
main--->fn3
main--->fn4
fn1--->fn1.1
fn1--->fn1.2
fn1--->fn1.3
fn1--->fn1.4
fn2--->fn2.1
fn2--->fn2.2
fn2--->fn2.3
fn2--->fn2.4
fn1.1-->fn1.1.1
fn1.1-->fn1.1.2
fn1.1-->fn1.1.3
fn1.1-->fn1.1.4
fn1.1-->fn1.1.5
If we adhere to this principle, building concurrent software becomes a lot easier, too. If the functionality and memory of fn1.1.1 and fn2 are completely separate, these can be run in parallel. Hence, our aim in software architecture must be looking for ways to represent the problem at hand as a tree structure, both physically (for memory) and temporally (for execution).