mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Structure stack/heap differently
Talk about what *will* be on the stack and what *will* be on the heap, rather than talking about "on the stack except for..."
This commit is contained in:
parent
178abe5dfe
commit
2f702ebbc5
@ -428,15 +428,27 @@ fastest allocator is the one you never use. As such, we're not going to go
|
|||||||
in detail on how exactly the
|
in detail on how exactly the
|
||||||
[`push` and `pop` instructions work](http://www.cs.virginia.edu/~evans/cs216/guides/x86.html),
|
[`push` and `pop` instructions work](http://www.cs.virginia.edu/~evans/cs216/guides/x86.html),
|
||||||
and we'll focus instead on the conditions that enable the Rust compiler to use
|
and we'll focus instead on the conditions that enable the Rust compiler to use
|
||||||
stack-based allocation for variables.
|
the faster stack-based allocation for variables.
|
||||||
|
|
||||||
Now, one question I hope you're asking is "how do we distinguish stack- and
|
With that in mind, let's get into the details. How do we know when Rust will or will not use
|
||||||
heap-based allocations in Rust code?" There are three strategies I'm going
|
stack allocation for objects we create? Looking at other languages, it's often easy to delineate
|
||||||
to use for this:
|
between stack and heap. Managed memory languages (Python, Java,
|
||||||
|
[C#](https://blogs.msdn.microsoft.com/ericlippert/2010/09/30/the-truth-about-value-types/)) assume
|
||||||
|
everything is on the heap. JIT compilers ([PyPy](https://www.pypy.org/),
|
||||||
|
[HotSpot](https://www.oracle.com/technetwork/java/javase/tech/index-jsp-136373.html)) may
|
||||||
|
optimize some heap allocations away, but you should never assume it will happen.
|
||||||
|
C makes things clear with calls to special functions ([malloc(3)](https://linux.die.net/man/3/malloc)
|
||||||
|
is one) being the way to use heap memory. Old C++ has the [`new`](https://stackoverflow.com/a/655086/1454178)
|
||||||
|
keyword, though modern C++/C++11 is more complicated with [RAII](https://en.cppreference.com/w/cpp/language/raii)
|
||||||
|
([`std::make_unique()`](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique) and
|
||||||
|
[`std::make_shared()`](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared))
|
||||||
|
|
||||||
1. When the stack pointer is modified to initialize a variable (done through either
|
For Rust specifically, the principle is this: *stack allocation will be used for everything
|
||||||
`push`/`pop` instructions or the `rsp` register being modified),
|
that doesn't involve "smart pointers" and collections.* If we're interested in proving
|
||||||
this is a stack allocation:
|
it though, there are three things to watch for:
|
||||||
|
|
||||||
|
1. Stack manipulation instructions (`push`, `pop`, and `add`/`sub` of the `rsp` register)
|
||||||
|
indicate allocation of stack memory:
|
||||||
```rust
|
```rust
|
||||||
pub fn stack_alloc(x: u32) -> u32 {
|
pub fn stack_alloc(x: u32) -> u32 {
|
||||||
// Space for `y` is allocated by subtracting from `rsp`,
|
// Space for `y` is allocated by subtracting from `rsp`,
|
||||||
@ -447,11 +459,10 @@ to use for this:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
-- [Compiler Explorer](https://godbolt.org/z/gKFOgB)
|
-- [Compiler Explorer](https://godbolt.org/z/gKFOgB)
|
||||||
2. Because there's a good deal of setup before heap allocations actually happen,
|
|
||||||
it's typically easier to watch for
|
2. Tracking when heap allocation calls happen is difficult. It's typically easier to
|
||||||
["dropping"](https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-rules)
|
watch for `call core::ptr::drop_in_place`, and infer that a heap allocation happened
|
||||||
variables instead. Any time `call core::ptr::drop_in_place` occurs, we can infer
|
in the recent past:
|
||||||
a heap allocation has occurred sometime in the past related to our variable:
|
|
||||||
```rust
|
```rust
|
||||||
pub fn heap_alloc(x: usize) -> usize {
|
pub fn heap_alloc(x: usize) -> usize {
|
||||||
// Space for elements in a vector has to be allocated
|
// Space for elements in a vector has to be allocated
|
||||||
@ -462,49 +473,55 @@ to use for this:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
-- [Compiler Explorer](https://godbolt.org/z/T2xoh8) (`drop_in_place` happens on line 1321)
|
-- [Compiler Explorer](https://godbolt.org/z/T2xoh8) (`drop_in_place` happens on line 1321)
|
||||||
|
<span style="font-size: .8em">Note: While the [`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html)
|
||||||
|
is called for stack-allocated objects, the Rust standard library only defines `Drop` implementations
|
||||||
|
for types that involve heap allocation.</span>
|
||||||
|
|
||||||
<span style="font-size: .8em">Note: While the [`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) is run
|
3. If you don't want to inspect the assembly, use a custom allocator that's able to track
|
||||||
for stack-allocated objects, the Rust standard library only defines `Drop` implementations
|
and alert when heap allocations occur. As an unashamed plug, [qadapt](https://crates.io/crates/qadapt)
|
||||||
for types that involve heap allocation.</span>
|
was designed for exactly this purpose.
|
||||||
3. Using a special [`GlobalAlloc`](https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html)
|
|
||||||
implementation to track when heap allocations occur. For this post, I'll be using
|
|
||||||
[qadapt](https://crates.io/crates/qadapt) to [trigger a panic](https://speice.io/2018/12/allocation-safety.html)
|
|
||||||
if heap allocations occur; code that doesn't panic doesn't use heap allocations.
|
|
||||||
|
|
||||||
With all that in mind, let's get into the details. How do we know when Rust will or will not use
|
With all that in mind, let's talk about situations in which we're guaranteed to use stack memory:
|
||||||
stack allocation for objects we create? Looking at other languages, it's often easy to identify
|
|
||||||
when this happens: Java only cares about `new MyObject()` (yes, I'm conveniently ignoring
|
|
||||||
autoboxing). C makes things clear with calls to [malloc(3)](https://linux.die.net/man/3/malloc),
|
|
||||||
and old C++ has the [`new`](https://stackoverflow.com/a/655086/1454178) keyword.
|
|
||||||
Modern C++ is a bit more complicated with C++11 and [RAII](https://en.cppreference.com/w/cpp/language/raii);
|
|
||||||
[`std::make_unique()`](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique) and
|
|
||||||
[`std::make_shared()`](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) are
|
|
||||||
used most often in this context (and are equivalent to [`Box`](https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html)
|
|
||||||
and [`Rc`](https://doc.rust-lang.org/stable/alloc/rc/struct.Rc.html) in Rust!).
|
|
||||||
|
|
||||||
For Rust specifically, the principle is this: *stack allocation will be used for all types
|
- Structs not wrapped by smart pointers are created on the stack.
|
||||||
that don't use "smart pointers" and collections.* We're going to expand on this to clarify
|
- Enums and unions are stack-allocated.
|
||||||
some common questions though:
|
- [Arrays](https://doc.rust-lang.org/std/primitive.array.html) are always stack-allocated.
|
||||||
|
|
||||||
**For code you control**:
|
|
||||||
|
|
||||||
- Smart pointer types (`Box`, `Rc`) and collections (`String`, `Vec`, `HashMap`)
|
|
||||||
force heap allocation for the data they manage.
|
|
||||||
- Enums and other wrapper types will not trigger heap allocations unless
|
|
||||||
their contents need heap allocation.
|
|
||||||
- [Arrays](https://doc.rust-lang.org/std/primitive.array.html) are guaranteed to be
|
|
||||||
stack-allocated, even if their size overflows available stack memory.
|
|
||||||
- Using the [`#[inline]` attribute](https://doc.rust-lang.org/reference/attributes.html#inline-attribute)
|
- Using the [`#[inline]` attribute](https://doc.rust-lang.org/reference/attributes.html#inline-attribute)
|
||||||
will not change the memory region used.
|
will not change the memory region used.
|
||||||
- [Closures](https://doc.rust-lang.org/reference/types/closure.html) obey the same
|
- Generics will use stack allocation, even with dynamic dispatch.
|
||||||
rules as `struct` and `enum` types; only closures wrapped in smart pointers
|
|
||||||
trigger an allocation.
|
|
||||||
|
|
||||||
**For code outside your control**: (crates you rely on)
|
## Enums
|
||||||
|
|
||||||
- Review the code to make sure it abides by the guidelines above
|
It's been a worry of mine that I'd manage to trigger a heap allocation because
|
||||||
- Use an allocator like [qadapt](https://crates.io/crates/qadapt) as an automated check
|
of wrapping an underlying type in
|
||||||
to make sure that stack allocations are used in code you care about.
|
Given that you're not using smart pointers, `enum` and other wrapper types will never use
|
||||||
|
heap allocations. This shows up most often with
|
||||||
|
[`Option`](https://doc.rust-lang.org/stable/core/option/enum.Option.html) and
|
||||||
|
[`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
|
||||||
|
discriminator, the compiler can predict how much memory is used. If enums were
|
||||||
|
sized as tightly as possible, heap allocations would be needed to handle the fact
|
||||||
|
that enum variants were of dynamic size!
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
## Smart pointers and collections
|
## Smart pointers and collections
|
||||||
|
|
||||||
@ -514,18 +531,28 @@ or your data is of unknown or dynamic size, you'll make use of these types.
|
|||||||
|
|
||||||
The term [smart pointer](https://en.wikipedia.org/wiki/Smart_pointer)
|
The term [smart pointer](https://en.wikipedia.org/wiki/Smart_pointer)
|
||||||
comes from C++, and is used to describe objects that are responsible for managing
|
comes from C++, and is used to describe objects that are responsible for managing
|
||||||
ownership of data allocated on the heap. In Rust, the smart pointers types are:
|
ownership of data allocated on the heap. Some familiar smart pointers come from the
|
||||||
|
low-level `alloc` crate:
|
||||||
- [`Box`](https://doc.rust-lang.org/alloc/boxed/struct.Box.html)
|
- [`Box`](https://doc.rust-lang.org/alloc/boxed/struct.Box.html)
|
||||||
- [`Rc`](https://doc.rust-lang.org/alloc/rc/struct.Rc.html)
|
- [`Rc`](https://doc.rust-lang.org/alloc/rc/struct.Rc.html)
|
||||||
- [`Arc`](https://doc.rust-lang.org/alloc/sync/struct.Arc.html)
|
- [`Arc`](https://doc.rust-lang.org/alloc/sync/struct.Arc.html)
|
||||||
- [`Cow`](https://doc.rust-lang.org/alloc/borrow/enum.Cow.html)
|
- [`Cow`](https://doc.rust-lang.org/alloc/borrow/enum.Cow.html)
|
||||||
|
|
||||||
|
The [standard library](https://doc.rust-lang.org/std/) also defines some smart pointers,
|
||||||
|
though more than can be covered in this article. Some examples:
|
||||||
|
- [`RwLock`](https://doc.rust-lang.org/std/sync/struct.RwLock.html)
|
||||||
|
- [`Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
|
||||||
|
|
||||||
|
Finally, there is one [gotcha](https://www.merriam-webster.com/dictionary/gotcha):
|
||||||
|
[`RefCell`](https://doc.rust-lang.org/stable/core/cell/struct.RefCell.html) looks like
|
||||||
|
and behaves like a smart pointer, but doesn't actually require heap allocation.
|
||||||
|
|
||||||
When a smart pointer is created, the data it is given is placed in heap memory and
|
When a smart pointer is created, the data it is given is placed in heap memory and
|
||||||
the location of that data is recorded in the smart pointer. Once the smart pointer
|
the location of that data is recorded in the smart pointer. Once the smart pointer
|
||||||
has determined it's safe to deallocate that memory (when a `Box` has
|
has determined it's safe to deallocate that memory (when a `Box` has
|
||||||
[gone out of scope](https://doc.rust-lang.org/stable/std/boxed/index.html) or when
|
[gone out of scope](https://doc.rust-lang.org/stable/std/boxed/index.html) or when
|
||||||
the [last reference](https://doc.rust-lang.org/alloc/rc/index.html) to an object
|
reference count for an object [goes to zero](https://doc.rust-lang.org/alloc/rc/index.html)),
|
||||||
is lost) the heap space is reclaimed. We can prove these types use heap memory by
|
the heap space is reclaimed. We can prove these types use heap memory by
|
||||||
looking at some quick code:
|
looking at some quick code:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@ -555,8 +582,7 @@ pub fn my_cow() {
|
|||||||
```
|
```
|
||||||
-- [Compiler Explorer](https://godbolt.org/z/QOPR4V)
|
-- [Compiler Explorer](https://godbolt.org/z/QOPR4V)
|
||||||
|
|
||||||
Collections types use heap memory because they have dynamic size; they will
|
Collections types use heap memory because they have dynamic size; they will request more memory
|
||||||
request more memory
|
|
||||||
[when they need it](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.reserve),
|
[when they need it](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.reserve),
|
||||||
and can be [asked to release memory](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.shrink_to_fit)
|
and can be [asked to release memory](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.shrink_to_fit)
|
||||||
when it's no longer necessary. This dynamic memory usage forces Rust to use
|
when it's no longer necessary. This dynamic memory usage forces Rust to use
|
||||||
@ -582,28 +608,6 @@ will ever be dispatched. A couple of places to look at for confirming this behav
|
|||||||
[`HashMap::new()`](https://doc.rust-lang.org/std/collections/hash_map/struct.HashMap.html#method.new),
|
[`HashMap::new()`](https://doc.rust-lang.org/std/collections/hash_map/struct.HashMap.html#method.new),
|
||||||
and [`String::new()`](https://doc.rust-lang.org/std/string/struct.String.html#method.new).
|
and [`String::new()`](https://doc.rust-lang.org/std/string/struct.String.html#method.new).
|
||||||
|
|
||||||
## Enums and Wrappers
|
|
||||||
|
|
||||||
# A Heaping Helping: Rust and Dynamic Memory
|
|
||||||
|
|
||||||
Example: How to trigger a heap allocation
|
|
||||||
|
|
||||||
Questions:
|
|
||||||
|
|
||||||
1. Where do collection types allocate memory?
|
|
||||||
2. Does a Box<> always allocate heap?
|
|
||||||
- Yes, with exception of compiler optimizations
|
|
||||||
3. Passing Box<Trait> vs. genericizing/monomorphization
|
|
||||||
- If it uses `dyn Trait`, it's on the heap?
|
|
||||||
- What if the trait implements `Sized`?
|
|
||||||
4. Other pointer types? Do Rc<>/Arc<> force heap allocation?
|
|
||||||
- Maybe? Part of the alloc crate, but should use qadapt to check
|
|
||||||
5. How many allocations happen before `main()` is called?
|
|
||||||
6. How can you use the heap well?
|
|
||||||
- Know when collections resizing happens
|
|
||||||
- Use `Borrow` to abstract over Pointer/Box/Rc/Arc/CoW
|
|
||||||
7. How expensive is move? Vs. C++ std::move?
|
|
||||||
|
|
||||||
# Compiler Optimizations: What It's Done For You Lately
|
# Compiler Optimizations: What It's Done For You Lately
|
||||||
|
|
||||||
1. Box<> getting inlined into stack allocations
|
1. Box<> getting inlined into stack allocations
|
||||||
|
1
assets/images/2019-01-28-container-size.svg
Normal file
1
assets/images/2019-01-28-container-size.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 426 KiB |
Loading…
Reference in New Issue
Block a user