Skip to content

Commit

Permalink
perf(transformer): introduce Stack (#6093)
Browse files Browse the repository at this point in the history
`Stack` is a stack structure, optimized for fast push/pop and reading/writing the last entry on the stack. The difference from `NonEmptyStack` is that it can be empty.

This has a different trade-off vs `NonEmptyStack`:

* `Stack::new` does not allocate (`NonEmptyStack` does).
* `Stack::last` and `Stack::last_mut` are fallible and contain a branch (those methods on `NonEmptyStack` are branchless and infallible).

`Stack` is only the better choice if:
1. You want `new()` not to allocate. or
2. Creating initial value for `NonEmptyStack::new()` is expensive.

Use `Stack` as one of the backing stores in `SparseStack`.
  • Loading branch information
overlookmotel committed Sep 27, 2024
1 parent ad4ef31 commit 85aff19
Show file tree
Hide file tree
Showing 4 changed files with 679 additions and 9 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_transformer/src/helpers/stack/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod capacity;
mod non_empty;
mod sparse;
mod standard;

use capacity::StackCapacity;
pub use non_empty::NonEmptyStack;
pub use sparse::SparseStack;
pub use standard::Stack;
5 changes: 4 additions & 1 deletion crates/oxc_transformer/src/helpers/stack/non_empty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use super::StackCapacity;
/// The fact that the stack is never empty makes all operations except `pop` infallible.
/// `last` and `last_mut` are branchless.
///
/// The trade-off is that you cannot create a `NonEmptyStack` without allocating (unlike `Vec`).
/// The trade-off is that you cannot create a `NonEmptyStack` without allocating.
/// If that is not a good trade-off for your use case, prefer [`Stack`], which can be empty.
///
/// To simplify implementation, zero size types are not supported (e.g. `NonEmptyStack<()>`).
///
Expand Down Expand Up @@ -47,6 +48,8 @@ use super::StackCapacity;
/// 2. Stack could grow downwards, like `bumpalo` allocator does. This would probably make `pop` use
/// 1 less register, but at the cost that the stack can never grow in place, which would incur more
/// memory copies when the stack grows.
///
/// [`Stack`]: super::Stack
pub struct NonEmptyStack<T> {
/// Pointer to last entry on stack.
/// Points *to* last entry, not *after* last entry.
Expand Down
21 changes: 13 additions & 8 deletions crates/oxc_transformer/src/helpers/stack/sparse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::NonEmptyStack;
use super::{NonEmptyStack, Stack};

/// Stack which is sparsely filled.
///
Expand All @@ -23,18 +23,23 @@ use super::NonEmptyStack;
///
/// When the stack grows and reallocates, `SparseStack` has less memory to copy, which is a performance
/// win too.
///
/// To simplify implementation, zero size types are not supported (`SparseStack<()>`).
pub struct SparseStack<T> {
has_values: NonEmptyStack<bool>,
values: Vec<T>,
values: Stack<T>,
}

impl<T> SparseStack<T> {
/// Create new `SparseStack`.
///
/// # Panics
/// Panics if `T` is a zero-sized type.
pub fn new() -> Self {
// `has_values` starts with a single empty entry, which will never be popped off.
// This means `take_last`, `last_or_init`, and `last_mut_or_init` can all be infallible,
// as there's always an entry on the stack to read.
Self { has_values: NonEmptyStack::new(false), values: vec![] }
Self { has_values: NonEmptyStack::new(false), values: Stack::new() }
}

/// Push an entry to the stack.
Expand Down Expand Up @@ -62,7 +67,7 @@ impl<T> SparseStack<T> {
// This invariant is maintained in `push`, `take_last`, `last_or_init`, and `last_mut_or_init`.
// We maintain it here too because we just popped from `self.has_values`, so that `true`
// has been consumed at the same time we consume its corresponding value from `self.values`.
let value = unsafe { self.values.pop().unwrap_unchecked() };
let value = unsafe { self.values.pop_unchecked() };
Some(value)
} else {
None
Expand All @@ -77,7 +82,7 @@ impl<T> SparseStack<T> {
debug_assert!(!self.values.is_empty());
// SAFETY: Last `self.has_values` is only `true` if there's a corresponding value in `self.values`.
// This invariant is maintained in `push`, `pop`, `take_last`, `last_or_init`, and `last_mut_or_init`.
let value = unsafe { self.values.last().unwrap_unchecked() };
let value = unsafe { self.values.last_unchecked() };
Some(value)
} else {
None
Expand All @@ -96,7 +101,7 @@ impl<T> SparseStack<T> {
// This invariant is maintained in `push`, `pop`, `last_or_init`, and `last_mut_or_init`.
// We maintain it here too because we just set last `self.has_values` to `false`
// at the same time as we consume the corresponding value from `self.values`.
let value = unsafe { self.values.pop().unwrap_unchecked() };
let value = unsafe { self.values.pop_unchecked() };
Some(value)
} else {
None
Expand All @@ -118,7 +123,7 @@ impl<T> SparseStack<T> {
// This invariant is maintained in `push`, `pop`, `take_last`, and `last_mut_or_init`.
// Here either last `self.has_values` was already `true`, or it's just been set to `true`
// and a value pushed to `self.values` above.
unsafe { self.values.last().unwrap_unchecked() }
unsafe { self.values.last_unchecked() }
}

/// Initialize the value for last entry on the stack, if it has no value already.
Expand All @@ -135,7 +140,7 @@ impl<T> SparseStack<T> {
// This invariant is maintained in `push`, `pop`, `take_last`, and `last_or_init`.
// Here either last `self.has_values` was already `true`, or it's just been set to `true`
// and a value pushed to `self.values` above.
unsafe { self.values.last_mut().unwrap_unchecked() }
unsafe { self.values.last_mut_unchecked() }
}

/// Get number of entries on the stack.
Expand Down
Loading

0 comments on commit 85aff19

Please sign in to comment.