mirror of
https://github.com/bspeice/speice.io
synced 2024-11-05 01:28:09 -05:00
Almost finished with stack memory
This commit is contained in:
parent
a7957a8c80
commit
6b2cb968a4
@ -91,32 +91,184 @@ there are three things we pay attention to:
|
|||||||
With all that in mind, let's talk about situations in which we're guaranteed to use stack memory:
|
With all that in mind, let's talk about situations in which we're guaranteed to use stack memory:
|
||||||
|
|
||||||
- Structs are created on the stack.
|
- Structs are created on the stack.
|
||||||
- Function arguments are passed on the stack.
|
- Function arguments are passed on the stack, meaning the
|
||||||
|
[`#[inline]` attribute](https://doc.rust-lang.org/reference/attributes.html#inline-attribute)
|
||||||
|
will not change the memory region used.
|
||||||
- Enums and unions are stack-allocated.
|
- Enums and unions are stack-allocated.
|
||||||
- [Arrays](https://doc.rust-lang.org/std/primitive.array.html) are always stack-allocated.
|
- [Arrays](https://doc.rust-lang.org/std/primitive.array.html) are always stack-allocated.
|
||||||
- Using the [`#[inline]` attribute](https://doc.rust-lang.org/reference/attributes.html#inline-attribute)
|
|
||||||
will not change the memory region used.
|
|
||||||
- Closures capture their arguments on the stack
|
- Closures capture their arguments on the stack
|
||||||
- Generics will use stack allocation, even with dynamic dispatch.
|
- Generics will use stack allocation, even with dynamic dispatch.
|
||||||
|
|
||||||
## Structs
|
## Structs
|
||||||
|
|
||||||
|
The simplest case comes first. When creating vanilla `struct` objects, we use stack memory
|
||||||
|
to hold their contents:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Point {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Line {
|
||||||
|
a: Point,
|
||||||
|
b: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_line() {
|
||||||
|
let origin = Point { x: 0., y: 0. };
|
||||||
|
let point = Point { x: 1., y: 2. };
|
||||||
|
|
||||||
|
let ray = Line { a: origin, b: point };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
-- [Compiler Explorer](https://godbolt.org/z/WZRa1D)
|
||||||
|
|
||||||
|
Note that while some extra-fancy instructions are used for memory manipulation in the assembly,
|
||||||
|
the `sub rsp, 64` instruction indicates we're still working with the stack.
|
||||||
|
|
||||||
|
## Function arguments
|
||||||
|
|
||||||
|
Have you ever wondered how functions communicate with each other? Like, once the variables are
|
||||||
|
given to you, everything's fine. But how do you "give" those variables to another function?
|
||||||
|
How do you get the results back afterward? The answer: the compiler arranges memory and
|
||||||
|
assembly instructions using a pre-determined
|
||||||
|
[calling convention](http://llvm.org/docs/LangRef.html#calling-conventions).
|
||||||
|
This convention governs the rules around where arguments needed by a function will be located
|
||||||
|
(either in memory offsets relative to the stack pointer `rsp`, or in other registers), and
|
||||||
|
where the results can be found once the function has finished. And when multiple languages
|
||||||
|
agree on what the calling conventions are, you can do things like having
|
||||||
|
[Go call Rust code](https://blog.filippo.io/rustgo/)!
|
||||||
|
|
||||||
|
Put simply: it's the compiler's job to figure out how to call other functions, and you can assume
|
||||||
|
that the compiler is good at its job.
|
||||||
|
|
||||||
|
We can see this in action using a simple example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Point {
|
||||||
|
x: i64,
|
||||||
|
y: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use integer division operations to keep
|
||||||
|
// the assembly clean, understanding the result
|
||||||
|
// isn't accurate.
|
||||||
|
fn distance(a: &Point, b: &Point) -> i64 {
|
||||||
|
// Immediately subtract from `rsp` the bytes needed
|
||||||
|
// to hold all the intermediate results - this is
|
||||||
|
// the stack allocation step
|
||||||
|
|
||||||
|
// The compiler used the `rdi` and `rsi` registers
|
||||||
|
// to pass our arguments, so read them in
|
||||||
|
let x1 = a.x;
|
||||||
|
let x2 = b.x;
|
||||||
|
let y1 = a.y;
|
||||||
|
let y2 = b.y;
|
||||||
|
|
||||||
|
// Do the actual math work
|
||||||
|
let x_pow = (x1 - x2) * (x1 - x2);
|
||||||
|
let y_pow = (y1 - y2) * (y1 - y2);
|
||||||
|
let squared = x_pow + y_pow;
|
||||||
|
squared / squared
|
||||||
|
|
||||||
|
// Our final result will be stored in the `rax` register
|
||||||
|
// so that our caller knows where to retrieve it.
|
||||||
|
// Finally, add back to `rsp` the stack memory that is
|
||||||
|
// now ready to be used by other functions.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_distance() {
|
||||||
|
let start = Point { x: 1, y: 2 };
|
||||||
|
let middle = Point { x: 3, y: 4 };
|
||||||
|
let end = Point { x: 5, y: 6 };
|
||||||
|
|
||||||
|
let _dist_1 = distance(&start, &middle);
|
||||||
|
let _dist_2 = distance(&middle, &end);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
-- [Compiler Explorer](https://godbolt.org/z/Qmx4ST)
|
||||||
|
|
||||||
|
As a consequence of function arguments never using heap memory, we can also
|
||||||
|
infer that functions using the `#[inline]` attributes also do not heap-allocate.
|
||||||
|
But better than inferring, we can look at the assembly to prove it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Point {
|
||||||
|
x: i64,
|
||||||
|
y: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that there is no `distance` function in the assembly output,
|
||||||
|
// and the total line count goes from 229 with inlining off
|
||||||
|
// to 306 with inline on. Even still, no heap allocations occur.
|
||||||
|
#[inline(always)]
|
||||||
|
fn distance(a: &Point, b: &Point) -> i64 {
|
||||||
|
let x1 = a.x;
|
||||||
|
let x2 = b.x;
|
||||||
|
let y1 = a.y;
|
||||||
|
let y2 = b.y;
|
||||||
|
|
||||||
|
let x_pow = (a.x - b.x) * (a.x - b.x);
|
||||||
|
let y_pow = (a.y - b.y) * (a.y - b.y);
|
||||||
|
let squared = x_pow + y_pow;
|
||||||
|
squared / squared
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_distance() {
|
||||||
|
let start = Point { x: 1, y: 2 };
|
||||||
|
let middle = Point { x: 3, y: 4 };
|
||||||
|
let end = Point { x: 5, y: 6 };
|
||||||
|
|
||||||
|
let _dist_1 = distance(&start, &middle);
|
||||||
|
let _dist_2 = distance(&middle, &end);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
-- [Compiler Explorer](https://godbolt.org/z/30Sh66)
|
||||||
|
|
||||||
|
Finally, passing by value (arguments with type
|
||||||
|
[`Copy`](https://doc.rust-lang.org/std/marker/trait.Copy.html))
|
||||||
|
and passing by reference (either moving ownership or passing a pointer) may have
|
||||||
|
[slightly different layouts in assembly](https://godbolt.org/z/sKi_kl), but will
|
||||||
|
still use either stack memory or CPU registers.
|
||||||
|
|
||||||
## Enums
|
## Enums
|
||||||
|
|
||||||
It's been a worry of mine that I'd manage to trigger a heap allocation because
|
If you've ever worried that wrapping your types in
|
||||||
of wrapping an underlying type in
|
[`Option`](https://doc.rust-lang.org/stable/core/option/enum.Option.html) or
|
||||||
Given that you're not using smart pointers, `enum` and other wrapper types will never use
|
[`Result`](https://doc.rust-lang.org/stable/core/result/enum.Result.html) would
|
||||||
heap allocations. This shows up most often with
|
finally make them large enough that Rust decides to use heap allocation instead,
|
||||||
[`Option`](https://doc.rust-lang.org/stable/core/option/enum.Option.html) and
|
fear no longer: `enum` and union types don't use heap allocation:
|
||||||
[`Result`](https://doc.rust-lang.org/stable/core/result/enum.Result.html) types,
|
|
||||||
but generalizes to any other types as well.
|
|
||||||
|
|
||||||
Because the size of an `enum` is the size of its largest element plus the size of a
|
```rust
|
||||||
discriminator, the compiler can predict how much memory is used. If enums were
|
enum MyEnum {
|
||||||
sized as tightly as possible, heap allocations would be needed to handle the fact
|
Small(u8),
|
||||||
that enum variants were of dynamic size!
|
Large(u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MyStruct {
|
||||||
|
x: MyEnum,
|
||||||
|
y: MyEnum,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enum_compare() {
|
||||||
|
let x = MyEnum::Small(0);
|
||||||
|
let y = MyEnum::Large(0);
|
||||||
|
|
||||||
|
let z = MyStruct { x, y };
|
||||||
|
|
||||||
|
let opt = Option::Some(z);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
-- [Compiler Explorer](https://godbolt.org/z/HK7zBx)
|
||||||
|
|
||||||
|
Because the size of an `enum` is the size of its largest element plus a flag,
|
||||||
|
the compiler can predict how much memory is used no matter which variant
|
||||||
|
of an enum is currently stored in a variable. Thus, enums and unions have no
|
||||||
|
need of heap allocation. There's unfortunately not a great way to show this
|
||||||
|
in assembly, so I'll instead point you to the
|
||||||
|
[`core::mem::size_of`](https://doc.rust-lang.org/stable/core/mem/fn.size_of.html#size-of-enums)
|
||||||
|
documentation.
|
||||||
|
|
||||||
## Arrays
|
## Arrays
|
||||||
|
|
||||||
@ -152,19 +304,16 @@ struct EightM {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Because we already have things in stack memory
|
// Because we already have things in stack memory
|
||||||
// (like the current function), allocating another
|
// (like the current function call stack), allocating another
|
||||||
// eight megabytes of stack memory crashes the program
|
// eight megabytes of stack memory crashes the program
|
||||||
let _x = EightM::default();
|
let _x = EightM::default();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
-- [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=137893e3ae05c2f32fe07d6f6f754709)
|
-- [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=587a6380a4914bcbcef4192c90c01dc4)
|
||||||
|
|
||||||
There aren't any security implications of this (no memory corruption occurs,
|
There aren't any security implications of this (no memory corruption occurs),
|
||||||
just running out of memory), but it's good to note that the Rust compiler
|
but it's good to note that the Rust compiler won't move arrays into heap memory
|
||||||
won't move arrays into heap memory even if they can be reasonably expected
|
even if they can be reasonably expected to overflow the stack.
|
||||||
to overflow the stack.
|
|
||||||
|
|
||||||
## **inline** attributes
|
|
||||||
|
|
||||||
## Closures
|
## Closures
|
||||||
|
|
||||||
@ -232,21 +381,3 @@ pub fn complex() {
|
|||||||
In every circumstance though, the compiler ensured that no heap allocations were necessary.
|
In every circumstance though, the compiler ensured that no heap allocations were necessary.
|
||||||
|
|
||||||
## Generics
|
## Generics
|
||||||
|
|
||||||
# A Heaping Helping: Rust and Dynamic Memory
|
|
||||||
|
|
||||||
Opening question: How many allocations happen before `fn main()` is called?
|
|
||||||
|
|
||||||
Now, one question I hope you're asking is "how do we distinguish stack- and
|
|
||||||
heap-based allocations in Rust code?" There are two strategies I'm going
|
|
||||||
to use for this:
|
|
||||||
|
|
||||||
Summary section:
|
|
||||||
|
|
||||||
- Smart pointers hold their contents in the heap
|
|
||||||
- Collections are smart pointers for many objects at a time, and reallocate
|
|
||||||
when they need to grow
|
|
||||||
- Boxed closures (FnBox, others?) are heap allocated
|
|
||||||
- "Move" semantics don't trigger new allocation; just a change of ownership,
|
|
||||||
so are incredibly fast
|
|
||||||
- Stack-based alternatives to standard library types should be preferred (spin, parking_lot)
|
|
Loading…
Reference in New Issue
Block a user