Skip to content

Commit

Permalink
Expand guide-level
Browse files Browse the repository at this point in the history
  • Loading branch information
c410-f3r committed Sep 28, 2020
1 parent cbabf74 commit 5082adc
Showing 1 changed file with 57 additions and 39 deletions.
96 changes: 57 additions & 39 deletions text/2978-stack_based_vec.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,56 +33,76 @@ Just like `Vec`, `ArrayVec` is also a primitive vector where high-level structur
# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Instead of relying on a heap-allocator, stack-based memory area is added and removed on-demand in a last-in-first-out (LIFO) order according to the calling workflow of a program. Let's illustrate an imaginary function `super()` that allocates 32 bits before calling another function named `sub()` that also allocates 32 bits of auxiliary memory:
`ArrayVec` is a container that encapsulates fixed size buffers.

```txt
0 bits 32 bits 64 bits
```rust
let mut v: ArrayVec<i32, 4> = ArrayVec::new();
let _ = v.push(1);
let _ = v.push(2);

assert_eq!(v.len(), 2);
assert_eq!(v[0], 1);

assert_eq!(v.pop(), Some(2));
assert_eq!(v.len(), 1);

v[0] = 7;
assert_eq!(v[0], 7);

v.extend([1, 2, 3].iter().copied());

| | -> | | -> | |
--------- --------- ---------
| super | | sub |
--------- ---------
| super |
---------
for element in &v {
println!("{}", element);
}
assert_eq!(v, [7, 1, 2, 3]);
```

* Now `super()` returns and the exactly same thing happens in reverse order, dropping everything when out of scope.
Instead of relying on a heap-allocator, stack-based memory area is added and removed on-demand in a last-in-first-out (LIFO) order according to the calling workflow of a program. `ArrayVec` takes advantage of this predictable behavior to reserve an exactly amount of uninitialized bytes up-front and these bytes form a buffer where elements can be included dynamically.

```txt
64 bits 32 bits 0 bits
```rust
// `array_vec` can store up to 64 elements
let mut array_vec: ArrayVec<i32, 64> = ArrayVec::new();
```

Of course, fixed buffers lead to inflexibility because unlike `Vec`, the underlying capacity can not expand at run-time and there will never be more than 64 elements in the above example.

| | -> | | -> | |
--------- --------- ---------
| sub | | super |
--------- ---------
| super |
---------
```rust
// This vector can store up to 0 elements, therefore, nothing at all
let mut array_vec: ArrayVec<i32, 0> = ArrayVec::new();
let push_result = array_vec.push(1);
// Ooppss... Our push operation wasn't successful
assert!(push_result.is_err());
```

`ArrayVec` takes advantage of this predictable behavior to reserve an exactly amount of uninitialized bytes up-front and these bytes form a buffer where elements can be included dynamically.
A good question is: Should I use `core::collections::ArrayVec<T>` or `alloc::collections::Vec<T>`? Well, `Vec` is already good enough for most situations while stack allocation usually shines for small sizes.

* Do you have a known upper bound?

* How much memory are you going to allocate for your program? The default values of `RUST_MIN_STACK` or `ulimit -s` might not be enough.

* Are you using nested `Vec`s? `Vec<ArrayVec<T, N>>` might be better than `Vec<Vec<T>>`.

Each use-case is different and should be pondered individually. In case of doubt, stick with `Vec`.

For a more technical overview, take a look at the following operations:

```rust
fn main() {
// `array_vec` has a pre-allocated memory of 2048 bits that can store up to 64 decimals.
let mut array_vec: ArrayVec<i32, 64> = ArrayVec::new();
// `array_vec` has a pre-allocated memory of 2048 bits (32 * 64) that can store up
// to 64 decimals.
let mut array_vec: ArrayVec<i32, 64> = ArrayVec::new();

// Although reserved, there isn't anything explicitly stored yet
assert_eq!(array_vec.len(), 0);
// Although reserved, there isn't anything explicitly stored yet
assert_eq!(array_vec.len(), 0);

// Initializes the first 32 bits with a simple '1' decimal or
// 00000000 00000000 00000000 00000001 bits
array_vec.push(1);
// Initializes the first 32 bits with a simple '1' decimal or
// 00000000 00000000 00000000 00000001 bits
array_vec.push(1);

// Our vector memory is now split into a 32/2016 pair of initialized and
// uninitialized memory respectively
assert_eq!(array_vec.len(), 1);
}
// Our vector memory is now split into a 32/2016 pair of initialized and
// uninitialized memory respectively
assert_eq!(array_vec.len(), 1);
```

Of course, fixed buffers lead to some inflexibility because unlike `Vec`, the underlying capacity can not expand at run-time and there will never be more than 64 elements in the above example.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

Expand Down Expand Up @@ -189,8 +209,6 @@ impl<T, const N: usize> ArrayVec<T, N> {
}
```

Since it is known at compile-time the upper capacity bound, the compiler is likely going to remove the conditional bounding checking of the newly

Meaningless, unstable and deprecated methods like `reserve` or `drain_filter` weren't considered. A concrete implementation is available at https://github.com/c410-f3r/stack-based-vec.

# Drawbacks
Expand Down Expand Up @@ -230,11 +248,11 @@ Should it be included in the prelude?
### Macros

```rust
// Instance with 1i32, 2i32 and 3i32
let _: ArrayVec<i32, 33> = array_vec![1, 2, 3];
// Instance with 1i32, 2i32 and 3i32
let _: ArrayVec<i32, 33> = array_vec![1, 2, 3];

// Instance with 1i32 and 1i32
let _: ArrayVec<i32, 64> = array_vec![1; 2];
// Instance with 1i32 and 1i32
let _: ArrayVec<i32, 64> = array_vec![1; 2];
```

# Future possibilities
Expand Down

0 comments on commit 5082adc

Please sign in to comment.