Skip to content

Commit fc1f817

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 acd22de commit fc1f817

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> $(extern $abi)? fn(&mut Executor<'store>, &[Instruction], usize, Instruction) -> ExecResult<()>;
33+
for<'store> $(extern $abi)? fn(&mut Executor<'store>, &[Instruction], usize, Instruction, usize) -> ExecResult<()>;
2734
#[allow(improper_ctypes_definitions)]
28-
type BoundedHandler = for<'store> $(extern $abi)? fn(&mut Executor<'store>, usize, Instruction, u32) -> ExecResult<()>;
35+
type BoundedHandler = for<'store> $(extern $abi)? fn(&mut Executor<'store>, 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 {
@@ -78,11 +87,12 @@ macro_rules! define_unbudgeted_tail_dispatch {
7887
}
7988
use tinywasm_types::Instruction::*;
8089
$(let $variant($($arg),*) = &instruction else {
81-
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction);
90+
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction, height32);
8291
};)?
8392
$(let $variant { $($field),* } = &instruction else {
84-
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction);
93+
become Self::handler_mismatch($executor, instructions, $instr_ptr, instruction, height32);
8594
};)?
95+
$executor.value_stack.stack_32.set_len(height32);
8696
$body;
8797
$dispatch_next!($instr_ptr + 1)
8898
}
@@ -110,10 +120,12 @@ macro_rules! define_bounded_tail_dispatch {
110120
$instr_ptr: usize,
111121
instruction: Instruction,
112122
instructions_until_checkpoint: u32,
123+
height32: usize,
113124
) -> ExecResult<()> {
114125
macro_rules! $dispatch_next {
115126
($next_instr_ptr:expr) => {{
116127
let next_instr_ptr = $next_instr_ptr;
128+
let height32 = $executor.value_stack.stack_32.len();
117129
if instructions_until_checkpoint == 0 {
118130
return cold!({
119131
$executor.cf.instr_ptr = next_instr_ptr;
@@ -122,10 +134,10 @@ macro_rules! define_bounded_tail_dispatch {
122134
}
123135

124136
let Some(&next) = $executor.func.instructions.get(next_instr_ptr) else {
125-
become Self::invalid_instr_ptr($executor, next_instr_ptr, instruction, instructions_until_checkpoint);
137+
become Self::invalid_instr_ptr($executor, next_instr_ptr, instruction, instructions_until_checkpoint, height32);
126138
};
127139
let handler = Self::handler_for(next.opcode());
128-
become handler($executor, next_instr_ptr, next, instructions_until_checkpoint - 1);
140+
become handler($executor, next_instr_ptr, next, instructions_until_checkpoint - 1, height32);
129141
}};
130142
}
131143
macro_rules! $dispatch_flow {
@@ -141,11 +153,12 @@ macro_rules! define_bounded_tail_dispatch {
141153
}
142154
use tinywasm_types::Instruction::*;
143155
$(let $variant($($arg),*) = &instruction else {
144-
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint);
156+
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint, height32);
145157
};)?
146158
$(let $variant { $($field),* } = &instruction else {
147-
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint);
159+
become Self::handler_mismatch($executor, $instr_ptr, instruction, instructions_until_checkpoint, height32);
148160
};)?
161+
$executor.value_stack.stack_32.set_len(height32);
149162
$body;
150163
$dispatch_next!($instr_ptr + 1)
151164
}
@@ -162,15 +175,15 @@ impl Unbudgeted {
162175
handler_fn! {
163176
#[cold]
164177
#[inline(never)]
165-
fn handler_mismatch(_: &mut Executor<'_>, _: &[Instruction], _: usize, _: Instruction) -> ExecResult<()> {
178+
fn handler_mismatch(_: &mut Executor<'_>, _: &[Instruction], _: usize, _: Instruction, _: usize) -> ExecResult<()> {
166179
unreachable!("instruction handler mismatch")
167180
}
168181
}
169182

170183
handler_fn! {
171184
#[cold]
172185
#[inline(never)]
173-
fn invalid_instr_ptr(_: &mut Executor<'_>, _: &[Instruction], instr_ptr: usize, _: Instruction) -> ExecResult<()> {
186+
fn invalid_instr_ptr(_: &mut Executor<'_>, _: &[Instruction], instr_ptr: usize, _: Instruction, _: usize) -> ExecResult<()> {
174187
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
175188
}
176189
}
@@ -184,15 +197,15 @@ impl Bounded {
184197
handler_fn! {
185198
#[cold]
186199
#[inline(never)]
187-
fn handler_mismatch(_: &mut Executor<'_>, _: usize, _: Instruction, _: u32) -> ExecResult<()> {
200+
fn handler_mismatch(_: &mut Executor<'_>, _: usize, _: Instruction, _: u32, _: usize) -> ExecResult<()> {
188201
unreachable!("instruction handler mismatch")
189202
}
190203
}
191204

192205
handler_fn! {
193206
#[cold]
194207
#[inline(never)]
195-
fn invalid_instr_ptr(_: &mut Executor<'_>, instr_ptr: usize, _: Instruction, _: u32) -> ExecResult<()> {
208+
fn invalid_instr_ptr(_: &mut Executor<'_>, instr_ptr: usize, _: Instruction, _: u32, _: usize) -> ExecResult<()> {
196209
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
197210
}
198211
}
@@ -202,7 +215,8 @@ impl Bounded {
202215
let instr_ptr = executor.cf.instr_ptr;
203216
let instruction = executor.func.instructions[instr_ptr];
204217
let handler = Self::handler_for(instruction.opcode());
205-
handler(executor, instr_ptr, instruction, CHECKPOINT_INTERVAL - 1)
218+
let height32 = executor.value_stack.stack_32.len();
219+
handler(executor, instr_ptr, instruction, CHECKPOINT_INTERVAL - 1, height32)
206220
}
207221
}
208222

@@ -215,7 +229,8 @@ impl<'store> Executor<'store> {
215229
let instr_ptr = self.cf.instr_ptr;
216230
let instruction = instructions[instr_ptr];
217231
let handler = Unbudgeted::handler_for(instruction.opcode());
218-
handler(&mut self, instructions, instr_ptr, instruction)?;
232+
let height32 = self.value_stack.stack_32.len();
233+
handler(&mut self, instructions, instr_ptr, instruction, height32)?;
219234
if self.completed {
220235
return Ok(());
221236
}

‎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)