Skip to content

Commit fe27adf

Browse files
authored
perf: grow the value stack out of line (#57)
First of a three-PR stack on `next` (`785be0e`), each on top of the previous one with individual wins that combine nicely: 1. #57 **perf: grow the value stack out of line** (this PR) 2. #58 perf: inline the fused binop and compare helpers 3. #59 perf: reserve each function's operand stack on entry `Stack::push` now checks `len == capacity` itself and hands the full case to a `#[cold] #[inline(never)] push_grow`, which keeps the existing overflow check and growth. After that check `Vec::push` can no longer reach its own growth path, so the allocator call leaves the instruction handlers. It was inlined into every handler that pushes, and the call made those handlers save and restore four pairs of callee-saved registers on every execution, even though the default config's stacks are fixed and never grow. `LocalGet32`'s hot path goes from 57 to 37 instructions on aarch64. No behavior change: fixed stacks still trap at capacity, and dynamic stacks still grow up to `max_size`. **iPhone 12 efficiency cores** (A14 Icestorm, `.utility` QoS). XCode 27 PMU template for low-level CPU stats. 16 benchmark rows, 5 launches per build, interleaved with the other builds of the stack; cycles and instructions per call, geomean against `next` as of `b45a98a`: | | cycles | instructions | rows faster | |---|---:|---:|---:| | this PR vs `next` | **−4.1 %** | −3.5 % | 16/16 | **Checks** on this branch: tinywasm's CI matrix run on my local M4 Mac: `cargo test --workspace` and `cargo test --workspace --examples` with `--features tinywasm/nightly-tail-calls`, each with and without default features
1 parent 785be0e commit fe27adf

1 file changed

Lines changed: 15 additions & 8 deletions

File tree

‎crates/tinywasm/src/interpreter/stack/value_stack.rs‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,29 @@ impl<T: Copy + Default> Stack<T> {
4646

4747
#[inline(always)]
4848
pub(crate) fn push(&mut self, value: T) -> Result<(), Trap> {
49-
// Check the limit only at capacity to avoid an extra hot-path check. Vec growth may
50-
// intentionally overshoot max_size. Revisit when Vec::push_within_capacity is stable.
51-
if self.data.len() == self.data.capacity() && (!self.dynamic || self.data.len() >= self.max_size) {
52-
return cold!(Err(Trap::ValueStackOverflow));
49+
// At capacity, grow (or trap) out of line. After this check `Vec::push` cannot reach its
50+
// own growth path, so the allocator call stays out of the instruction handlers.
51+
if self.data.len() == self.data.capacity() {
52+
return self.push_grow(value);
5353
}
5454
self.data.push(value);
5555
Ok(())
5656
}
5757

5858
#[inline(always)]
5959
pub(crate) fn push_copy(&mut self, index: usize) -> Result<(), Trap> {
60-
// Keep the same capacity-based limit check as push, including intentional overshoot.
61-
if self.data.len() == self.data.capacity() && (!self.dynamic || self.data.len() >= self.max_size) {
62-
return cold!(Err(Trap::ValueStackOverflow));
63-
}
6460
let value = self.data[index];
61+
self.push(value)
62+
}
63+
64+
#[cold]
65+
#[inline(never)]
66+
fn push_grow(&mut self, value: T) -> Result<(), Trap> {
67+
// Check the limit only at capacity to avoid an extra hot-path check. Vec growth may
68+
// intentionally overshoot max_size.
69+
if !self.dynamic || self.data.len() >= self.max_size {
70+
return Err(Trap::ValueStackOverflow);
71+
}
6572
self.data.push(value);
6673
Ok(())
6774
}

0 commit comments

Comments
 (0)