Skip to content

Commit d42907d

Browse files
committed
perf: pass the 32-bit value stack's height through the tail-call handlers
Each tail-call handler receives the height of the 32-bit value stack, which most instructions push to or pop from, as its last argument. It writes the height to the stack before its body and reads it back before dispatching, and with the stack in the executor the compiler can keep it in a register in between, as it does in most handlers. A handler no longer loads the height its predecessor stored, a store-to-load round trip through memory on every dispatch. The Unbudgeted handlers now take six integer arguments and the Bounded ones five, which stay in registers on arm64 (eight) and x86-64 System V (six). Windows x64 passes four, so there the height goes on the stack along with the Instruction, which already does without this change.
1 parent 8f687b2 commit d42907d

2 files changed

Lines changed: 42 additions & 17 deletions

File tree

‎crates/tinywasm/src/interpreter/executor/dispatch_become.rs‎

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ use super::*;
33
struct Unbudgeted;
44
struct Bounded;
55

6+
// The last argument of both handler types is the height of the 32-bit value stack, which most
7+
// instructions push to or pop from. Each handler writes it back to the stack before its body runs
8+
// and reads it again before dispatching, so no handler loads the height its predecessor stored.
9+
// Most keep it in a register in between. A few load it back before dispatching: the 8- and 16-lane
10+
// SIMD ops, and the fused `LoadLocal*` handlers, whose hot path joins a cold call.
11+
//
612
// Between two handlers only what the calling convention passes in registers stays out of memory.
713
// On arm64_32 (watchOS) the Rust ABI passes an aggregate larger than a pointer, the 8-byte
814
// `Instruction`, by reference, so every dispatch would store it and the next handler load it back.
915
// The C convention passes it in a register; `C-unwind` still lets a host function's panic unwind.
10-
// Elsewhere the handlers keep the Rust ABI.
16+
// Elsewhere the handlers keep the Rust ABI, which passes eight integer arguments in registers on
17+
// arm64, six on x86-64 System V (as many as the Unbudgeted handlers take) and four on Windows x64.
1118
macro_rules! handler_fn {
1219
($(#[$meta:meta])* fn $($rest:tt)*) => {
1320
#[cfg(all(target_arch = "aarch64", target_pointer_width = "32"))]
@@ -23,9 +30,9 @@ macro_rules! handler_types {
2330
// Both sides are Rust, so the C convention's view of these types need not be FFI-safe.
2431
#[allow(improper_ctypes_definitions)]
2532
type UnbudgetedHandler =
26-
for<'store, 'module> $(extern $abi)? fn(&mut Executor<'store, 'module>, &[Instruction], usize, Instruction) -> ExecResult<()>;
33+
for<'store, 'module> $(extern $abi)? fn(&mut Executor<'store, 'module>, &[Instruction], usize, Instruction, usize) -> ExecResult<()>;
2734
#[allow(improper_ctypes_definitions)]
28-
type BoundedHandler = for<'store, 'module> $(extern $abi)? fn(&mut Executor<'store, 'module>, usize, Instruction, u32) -> ExecResult<()>;
35+
type BoundedHandler = for<'store, 'module> $(extern $abi)? fn(&mut Executor<'store, 'module>, usize, Instruction, u32, usize) -> ExecResult<()>;
2936
};
3037
}
3138
#[cfg(all(target_arch = "aarch64", target_pointer_width = "32"))]
@@ -53,15 +60,17 @@ macro_rules! define_unbudgeted_tail_dispatch {
5360
instructions: &[Instruction],
5461
$instr_ptr: usize,
5562
instruction: Instruction,
63+
height32: usize,
5664
) -> ExecResult<()> {
5765
macro_rules! $dispatch_next {
5866
($next_instr_ptr:expr) => {{
5967
let next_instr_ptr = $next_instr_ptr;
68+
let height32 = $executor.value_stack.stack_32.len();
6069
let Some(&next) = instructions.get(next_instr_ptr) else {
61-
become Self::invalid_instr_ptr($executor, instructions, next_instr_ptr, instruction);
70+
become Self::invalid_instr_ptr($executor, instructions, next_instr_ptr, instruction, height32);
6271
};
6372
let handler = Self::handler_for(next.opcode());
64-
become handler($executor, instructions, next_instr_ptr, next);
73+
become handler($executor, instructions, next_instr_ptr, next, height32);
6574
}};
6675
}
6776
macro_rules! $dispatch_flow {
@@ -83,11 +92,12 @@ macro_rules! define_unbudgeted_tail_dispatch {
8392
}
8493
use tinywasm_types::Instruction::*;
8594
$(let $variant($($arg),*) = &instruction else {
86-
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction);
95+
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction, height32);
8796
};)?
8897
$(let $variant { $($field),* } = &instruction else {
89-
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction);
98+
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction, height32);
9099
};)?
100+
$executor.value_stack.stack_32.set_len(height32);
91101
$body;
92102
$dispatch_next!($instr_ptr + 1)
93103
}
@@ -115,10 +125,12 @@ macro_rules! define_bounded_tail_dispatch {
115125
$instr_ptr: usize,
116126
instruction: Instruction,
117127
instructions_until_checkpoint: u32,
128+
height32: usize,
118129
) -> ExecResult<()> {
119130
macro_rules! $dispatch_next {
120131
($next_instr_ptr:expr) => {{
121132
let next_instr_ptr = $next_instr_ptr;
133+
let height32 = $executor.value_stack.stack_32.len();
122134
if instructions_until_checkpoint == 0 {
123135
return cold!({
124136
$executor.cf.instr_ptr = next_instr_ptr;
@@ -127,10 +139,10 @@ macro_rules! define_bounded_tail_dispatch {
127139
}
128140

129141
let Some(&next) = $executor.func.instructions.get(next_instr_ptr) else {
130-
become Self::invalid_instr_ptr($executor, next_instr_ptr, instruction, instructions_until_checkpoint);
142+
become Self::invalid_instr_ptr($executor, next_instr_ptr, instruction, instructions_until_checkpoint, height32);
131143
};
132144
let handler = Self::handler_for(next.opcode());
133-
become handler($executor, next_instr_ptr, next, instructions_until_checkpoint - 1);
145+
become handler($executor, next_instr_ptr, next, instructions_until_checkpoint - 1, height32);
134146
}};
135147
}
136148
macro_rules! $dispatch_flow {
@@ -150,11 +162,12 @@ macro_rules! define_bounded_tail_dispatch {
150162
}
151163
use tinywasm_types::Instruction::*;
152164
$(let $variant($($arg),*) = &instruction else {
153-
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint);
165+
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint, height32);
154166
};)?
155167
$(let $variant { $($field),* } = &instruction else {
156-
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint);
168+
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint, height32);
157169
};)?
170+
$executor.value_stack.stack_32.set_len(height32);
158171
$body;
159172
$dispatch_next!($instr_ptr + 1)
160173
}
@@ -171,15 +184,15 @@ impl Unbudgeted {
171184
handler_fn! {
172185
#[cold]
173186
#[inline(never)]
174-
fn handler_mismatch(_: &mut Executor<'_, '_>, _: &[Instruction], _: usize, _: Instruction) -> ExecResult<()> {
187+
fn handler_mismatch(_: &mut Executor<'_, '_>, _: &[Instruction], _: usize, _: Instruction, _: usize) -> ExecResult<()> {
175188
unreachable!("instruction handler mismatch")
176189
}
177190
}
178191

179192
handler_fn! {
180193
#[cold]
181194
#[inline(never)]
182-
fn invalid_instr_ptr(_: &mut Executor<'_, '_>, _: &[Instruction], instr_ptr: usize, _: Instruction) -> ExecResult<()> {
195+
fn invalid_instr_ptr(_: &mut Executor<'_, '_>, _: &[Instruction], instr_ptr: usize, _: Instruction, _: usize) -> ExecResult<()> {
183196
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
184197
}
185198
}
@@ -193,15 +206,15 @@ impl Bounded {
193206
handler_fn! {
194207
#[cold]
195208
#[inline(never)]
196-
fn handler_mismatch(_: &mut Executor<'_, '_>, _: usize, _: Instruction, _: u32) -> ExecResult<()> {
209+
fn handler_mismatch(_: &mut Executor<'_, '_>, _: usize, _: Instruction, _: u32, _: usize) -> ExecResult<()> {
197210
unreachable!("instruction handler mismatch")
198211
}
199212
}
200213

201214
handler_fn! {
202215
#[cold]
203216
#[inline(never)]
204-
fn invalid_instr_ptr(_: &mut Executor<'_, '_>, instr_ptr: usize, _: Instruction, _: u32) -> ExecResult<()> {
217+
fn invalid_instr_ptr(_: &mut Executor<'_, '_>, instr_ptr: usize, _: Instruction, _: u32, _: usize) -> ExecResult<()> {
205218
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
206219
}
207220
}
@@ -212,7 +225,8 @@ impl Bounded {
212225
let instr_ptr = executor.cf.instr_ptr;
213226
let instruction = executor.func.instructions[instr_ptr];
214227
let handler = Self::handler_for(instruction.opcode());
215-
handler(executor, instr_ptr, instruction, chunk_left - 1)
228+
let height32 = executor.value_stack.stack_32.len();
229+
handler(executor, instr_ptr, instruction, chunk_left - 1, height32)
216230
}
217231
}
218232

@@ -226,7 +240,8 @@ impl Executor<'_, '_> {
226240
let instr_ptr = self.cf.instr_ptr;
227241
let instruction = instructions[instr_ptr];
228242
let handler = Unbudgeted::handler_for(instruction.opcode());
229-
handler(&mut self, instructions, instr_ptr, instruction)?;
243+
let height32 = self.value_stack.stack_32.len();
244+
handler(&mut self, instructions, instr_ptr, instruction, height32)?;
230245
if self.completed || self.left {
231246
return Ok(self.left());
232247
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ impl<T: Copy + Default> Stack<T> {
6262
self.len
6363
}
6464

65+
/// Sets the height to one read from [`Self::len`] earlier, with nothing pushed or popped since.
66+
/// The tail-call handlers pass the height between them and write it back on entry, so the
67+
/// compiler can use the value in a register instead of reloading it from memory.
68+
#[cfg(feature = "nightly-tail-calls")]
69+
#[inline(always)]
70+
pub(crate) fn set_len(&mut self, len: usize) {
71+
debug_assert!(len <= self.data.len());
72+
self.len = len;
73+
}
74+
6575
/// Pushes a value inside a function body. `enter_locals` reserved the function's whole operand
6676
/// stack, so the stack is never full here, and the handlers make no calls.
6777
#[inline(always)]

0 commit comments

Comments
 (0)