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:
Bradlee Speice 2019-01-27 16:39:42 -05:00
parent 178abe5dfe
commit 2f702ebbc5
2 changed files with 80 additions and 75 deletions

View File

@ -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)
<span style="font-size: .8em">Note: While the [`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) is run is called for stack-allocated objects, the Rust standard library only defines `Drop` implementations
for stack-allocated objects, the Rust standard library only defines `Drop` implementations
for types that involve heap allocation.</span> for types that involve heap allocation.</span>
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 3. If you don't want to inspect the assembly, use a custom allocator that's able to track
stack allocation for objects we create? Looking at other languages, it's often easy to identify and alert when heap allocations occur. As an unashamed plug, [qadapt](https://crates.io/crates/qadapt)
when this happens: Java only cares about `new MyObject()` (yes, I'm conveniently ignoring was designed for exactly this purpose.
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 With all that in mind, let's talk about situations in which we're guaranteed to use stack memory:
that don't use "smart pointers" and collections.* We're going to expand on this to clarify
some common questions though:
**For code you control**: - Structs not wrapped by smart pointers are created on the stack.
- Enums and unions are stack-allocated.
- Smart pointer types (`Box`, `Rc`) and collections (`String`, `Vec`, `HashMap`) - [Arrays](https://doc.rust-lang.org/std/primitive.array.html) are always stack-allocated.
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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 426 KiB