Skip to content

Commit 3a1723e

Browse files
committed
perf: pick the tail-call handlers' calling convention per target
The Unbudgeted handlers take five integer arguments. The Rust ABI passes six in registers on x86-64 System V but four on Windows, which puts the Instruction on the stack on every dispatch there. `rust-preserve-none` (nightly, like `become`) passes twelve in registers on every OS and keeps only RSP and RBP callee-saved, so a handler saves at most RBP. On arm64_32 (watchOS) the Rust ABI passes the 8-byte Instruction by reference, so every dispatch stores it and the next handler loads it back. The C convention passes it in a register, and `C-unwind` still lets a host function's panic unwind. Other targets keep the Rust ABI.
1 parent 017780e commit 3a1723e

2 files changed

Lines changed: 68 additions & 22 deletions

File tree

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

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,45 @@ use super::*;
33
struct Unbudgeted;
44
struct Bounded;
55

6-
type UnbudgetedHandler = for<'store> fn(&mut Executor<'store>, &[Instruction], usize, Instruction) -> ExecResult<()>;
7-
type BoundedHandler = for<'store> fn(&mut Executor<'store>, usize, Instruction, u32) -> ExecResult<()>;
6+
// Between two handlers only what the calling convention passes in registers stays out of memory,
7+
// so the handlers' convention depends on the target:
8+
// - x86-64: the Rust ABI passes six integer arguments in registers on System V but four on
9+
// Windows, which would put the Instruction on the stack. `rust-preserve-none`
10+
// passes twelve on every OS and keeps only RSP and RBP callee-saved, so a handler saves at most
11+
// RBP, and only when it uses it.
12+
// - arm64_32 (watchOS): the Rust ABI passes an aggregate larger than a pointer, the 8-byte
13+
// `Instruction`, by reference, so every dispatch would store it and the next handler load it
14+
// back. The C convention passes it in a register; `C-unwind` still lets a host function's
15+
// panic unwind.
16+
// - elsewhere, the Rust ABI (arm64 passes 8 integer arguments in registers).
17+
macro_rules! handler_fn {
18+
($(#[$meta:meta])* fn $($rest:tt)*) => {
19+
#[cfg(target_arch = "x86_64")]
20+
$(#[$meta])* extern "rust-preserve-none" fn $($rest)*
21+
#[cfg(all(target_arch = "aarch64", target_pointer_width = "32"))]
22+
#[allow(improper_ctypes_definitions)]
23+
$(#[$meta])* extern "C-unwind" fn $($rest)*
24+
#[cfg(not(any(target_arch = "x86_64", all(target_arch = "aarch64", target_pointer_width = "32"))))]
25+
$(#[$meta])* fn $($rest)*
26+
};
27+
}
28+
29+
macro_rules! handler_types {
30+
($($abi:literal)?) => {
31+
// Both sides are Rust, so the C convention's view of these types need not be FFI-safe.
32+
#[allow(improper_ctypes_definitions)]
33+
type UnbudgetedHandler =
34+
for<'store> $(extern $abi)? fn(&mut Executor<'store>, &[Instruction], usize, Instruction) -> ExecResult<()>;
35+
#[allow(improper_ctypes_definitions)]
36+
type BoundedHandler = for<'store> $(extern $abi)? fn(&mut Executor<'store>, usize, Instruction, u32) -> ExecResult<()>;
37+
};
38+
}
39+
#[cfg(target_arch = "x86_64")]
40+
handler_types!("rust-preserve-none");
41+
#[cfg(all(target_arch = "aarch64", target_pointer_width = "32"))]
42+
handler_types!("C-unwind");
43+
#[cfg(not(any(target_arch = "x86_64", all(target_arch = "aarch64", target_pointer_width = "32"))))]
44+
handler_types!();
845

946
macro_rules! define_unbudgeted_tail_dispatch {
1047
($executor:ident, $instr_ptr:ident, $dispatch_next:ident, $dispatch_flow:ident;
@@ -19,7 +56,7 @@ macro_rules! define_unbudgeted_tail_dispatch {
1956
HANDLERS[opcode as usize]
2057
}
2158

22-
$(
59+
$(handler_fn! {
2360
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
2461
fn $variant(
2562
$executor: &mut Executor<'_>,
@@ -59,7 +96,7 @@ macro_rules! define_unbudgeted_tail_dispatch {
5996
$body;
6097
$dispatch_next!($instr_ptr + 1)
6198
}
62-
)*
99+
})*
63100
};
64101
}
65102

@@ -76,7 +113,7 @@ macro_rules! define_bounded_tail_dispatch {
76113
HANDLERS[opcode as usize]
77114
}
78115

79-
$(
116+
$(handler_fn! {
80117
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
81118
fn $variant(
82119
$executor: &mut Executor<'_>,
@@ -122,7 +159,7 @@ macro_rules! define_bounded_tail_dispatch {
122159
$body;
123160
$dispatch_next!($instr_ptr + 1)
124161
}
125-
)*
162+
})*
126163
};
127164
}
128165

@@ -132,16 +169,20 @@ impl Unbudgeted {
132169
// The handlers tail-call these cold paths instead of calling them: a call would make every
133170
// handler save a stack frame.
134171

135-
#[cold]
136-
#[inline(never)]
137-
fn handler_mismatch(_: &mut Executor<'_>, _: &[Instruction], _: usize, _: Instruction) -> ExecResult<()> {
138-
unreachable!("instruction handler mismatch")
172+
handler_fn! {
173+
#[cold]
174+
#[inline(never)]
175+
fn handler_mismatch(_: &mut Executor<'_>, _: &[Instruction], _: usize, _: Instruction) -> ExecResult<()> {
176+
unreachable!("instruction handler mismatch")
177+
}
139178
}
140179

141-
#[cold]
142-
#[inline(never)]
143-
fn invalid_instr_ptr(_: &mut Executor<'_>, _: &[Instruction], instr_ptr: usize, _: Instruction) -> ExecResult<()> {
144-
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
180+
handler_fn! {
181+
#[cold]
182+
#[inline(never)]
183+
fn invalid_instr_ptr(_: &mut Executor<'_>, _: &[Instruction], instr_ptr: usize, _: Instruction) -> ExecResult<()> {
184+
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
185+
}
145186
}
146187
}
147188

@@ -150,16 +191,20 @@ impl Bounded {
150191

151192
// Tail-called like `Unbudgeted`'s.
152193

153-
#[cold]
154-
#[inline(never)]
155-
fn handler_mismatch(_: &mut Executor<'_>, _: usize, _: Instruction, _: u32) -> ExecResult<()> {
156-
unreachable!("instruction handler mismatch")
194+
handler_fn! {
195+
#[cold]
196+
#[inline(never)]
197+
fn handler_mismatch(_: &mut Executor<'_>, _: usize, _: Instruction, _: u32) -> ExecResult<()> {
198+
unreachable!("instruction handler mismatch")
199+
}
157200
}
158201

159-
#[cold]
160-
#[inline(never)]
161-
fn invalid_instr_ptr(_: &mut Executor<'_>, instr_ptr: usize, _: Instruction, _: u32) -> ExecResult<()> {
162-
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
202+
handler_fn! {
203+
#[cold]
204+
#[inline(never)]
205+
fn invalid_instr_ptr(_: &mut Executor<'_>, instr_ptr: usize, _: Instruction, _: u32) -> ExecResult<()> {
206+
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
207+
}
163208
}
164209

165210
#[inline(always)]

‎crates/tinywasm/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#![cfg_attr(feature = "nightly-tail-calls", allow(incomplete_features))]
33
#![cfg_attr(feature = "nightly-tail-calls", feature(explicit_tail_calls))]
44
#![cfg_attr(all(feature = "nightly-tail-calls", not(debug_assertions)), feature(abort_immediate))]
5+
#![cfg_attr(all(feature = "nightly-tail-calls", target_arch = "x86_64"), feature(rust_preserve_none_cc))]
56
#![doc(test(
67
no_crate_inject,
78
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_assignments, unused_variables))

0 commit comments

Comments
 (0)