Skip to content

Commit bae7a71

Browse files
committed
perf: borrow the executing function from its module instance
Every call and return between two functions cloned the callee's `Shared<WasmFunction>` and dropped the previous one, two refcount updates each way, and looked the function up in the store. A module instance now keeps its own functions, which the store allocates contiguously. The interpreter holds the instance for the whole run, so the executor borrows the executing function and its module from it: a call or return within the instance switches a reference, and a direct call to one of the module's own functions skips the address table, the host check and the owner check. Execution that continues in another instance's function (a call through an import, table or reference, a return, or an exception unwinding into it) ends the run, and `InterpreterRuntime` resumes that frame with an executor for its instance. Fuel and time budgets carry over, so a run suspends at the same points as before.
1 parent 77cbbb5 commit bae7a71

7 files changed

Lines changed: 502 additions & 149 deletions

File tree

‎crates/tinywasm/src/instance.rs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ struct ModuleInstanceInner {
6060
id: ModuleInstanceId,
6161
type_addrs: Box<[TypeAddr]>,
6262
func_addrs: Box<[FuncAddr]>,
63+
/// The module's own (non-imported) functions, in index order. The store allocates them
64+
/// contiguously from `local_func_base`, so an executor can borrow any of them for a whole run.
65+
local_funcs: Box<[Shared<WasmFunction>]>,
66+
local_func_base: FuncAddr,
67+
imported_funcs: u32,
6368
table_addrs: Box<[TableAddr]>,
6469
mem_addrs: Box<[MemAddr]>,
6570
#[cfg(feature = "std")]
@@ -84,6 +89,20 @@ impl ModuleInstance {
8489
self.0.func_addrs[addr as usize]
8590
}
8691

92+
/// The store address and body of the module's own function `idx`, or `None` for an import.
93+
#[inline(always)]
94+
pub(crate) fn local_func_by_index(&self, idx: FuncAddr) -> Option<(FuncAddr, &WasmFunction)> {
95+
let local = idx.wrapping_sub(self.0.imported_funcs);
96+
let func = self.0.local_funcs.get(local as usize)?;
97+
Some((self.0.local_func_base + local, func))
98+
}
99+
100+
/// The body of the function at store address `addr`, if this instance owns it.
101+
#[inline(always)]
102+
pub(crate) fn local_func(&self, addr: FuncAddr) -> Option<&WasmFunction> {
103+
self.0.local_funcs.get(addr.wrapping_sub(self.0.local_func_base) as usize).map(|func| &**func)
104+
}
105+
87106
/// resolve a table address to the global store address
88107
#[inline]
89108
pub(crate) fn resolve_table_addr(&self, addr: TableAddr) -> TableAddr {
@@ -211,7 +230,9 @@ impl ModuleInstance {
211230
let id = store.next_module_instance_id();
212231
let mut addrs = crate::imports::ResolvedImports::new(store, module, &type_addrs, imports)?;
213232
let imported_funcs = addrs.funcs.len();
214-
addrs.funcs.extend(store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs));
233+
let local_funcs = store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs);
234+
let local_func_base = local_funcs.start;
235+
addrs.funcs.extend(local_funcs);
215236
addrs.tags.extend(store.init_tags(&module.tags, &type_addrs));
216237
let limiter = store.engine.config().resource_limiter.clone();
217238
if !module.skip_local_memory_allocation {
@@ -238,6 +259,9 @@ impl ModuleInstance {
238259
id,
239260
type_addrs,
240261
func_addrs: addrs.funcs.into_boxed_slice(),
262+
local_funcs: module.funcs.clone(),
263+
local_func_base,
264+
imported_funcs: imported_funcs as u32,
241265
table_addrs: addrs.tables.into_boxed_slice(),
242266
mem_addrs: addrs.memories.into_boxed_slice(),
243267
#[cfg(feature = "std")]

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

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,67 +24,76 @@ macro_rules! define_stable_dispatch {
2424
};
2525
}
2626

27-
impl Executor<'_> {
27+
impl Executor<'_, '_> {
2828
instruction_handlers!(define_stable_dispatch);
2929

30+
/// Runs until the call completes (`None`) or continues in another module instance's frame.
3031
#[inline(always)]
31-
pub(crate) fn run_to_completion(mut self) -> Result<()> {
32+
pub(crate) fn run_to_completion(mut self) -> Result<Option<CallFrame>> {
3233
let mut instr_ptr = self.cf.instr_ptr;
3334
loop {
3435
match Self::exec_step(&mut self, instr_ptr)?.next_instr_ptr() {
3536
Some(next_instr_ptr) => instr_ptr = next_instr_ptr,
36-
None => return cold!(Ok(())),
37+
None => return cold!(Ok(self.left())),
3738
}
3839
}
3940
}
4041

42+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until `time_budget` has
43+
/// elapsed since `start`.
4144
#[cfg(feature = "std")]
4245
#[inline(always)]
43-
pub(crate) fn run_with_time_budget(mut self, time_budget: core::time::Duration) -> Result<ExecState> {
44-
use crate::std::time::Instant;
45-
46-
if time_budget.is_zero() {
47-
return Ok(ExecState::Suspended(self.cf));
48-
}
49-
let start = Instant::now();
46+
pub(crate) fn run_with_time_budget(
47+
mut self,
48+
start: crate::std::time::Instant,
49+
time_budget: core::time::Duration,
50+
mut chunk_left: u32,
51+
) -> Result<RunEnd> {
5052
let mut instr_ptr = self.cf.instr_ptr;
5153
loop {
52-
for _ in 0..CHECKPOINT_INTERVAL {
54+
while chunk_left != 0 {
55+
chunk_left -= 1;
5356
match Self::exec_step(&mut self, instr_ptr)?.next_instr_ptr() {
5457
Some(next_instr_ptr) => instr_ptr = next_instr_ptr,
55-
None => return Ok(ExecState::Completed),
58+
None => return Ok(self.run_end(chunk_left)),
5659
}
5760
}
61+
chunk_left = CHECKPOINT_INTERVAL;
5862

5963
if start.elapsed() >= time_budget {
6064
self.cf.instr_ptr = instr_ptr;
61-
return Ok(ExecState::Suspended(self.cf));
65+
return Ok(RunEnd::State(ExecState::Suspended(self.cf)));
6266
}
6367
}
6468
}
6569

70+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until the store's fuel is out.
6671
#[inline(always)]
67-
pub(crate) fn run_with_fuel(mut self, fuel: u32) -> Result<ExecState> {
72+
pub(crate) fn run_with_fuel(mut self, mut chunk_left: u32) -> Result<RunEnd> {
6873
self.fuel_metered = true;
69-
self.store.execution_fuel = fuel;
70-
if self.store.execution_fuel == 0 {
71-
return Ok(ExecState::Suspended(self.cf));
72-
}
73-
7474
let mut instr_ptr = self.cf.instr_ptr;
7575
loop {
76-
for _ in 0..CHECKPOINT_INTERVAL {
76+
while chunk_left != 0 {
77+
chunk_left -= 1;
7778
match Self::exec_step(&mut self, instr_ptr)?.next_instr_ptr() {
7879
Some(next_instr_ptr) => instr_ptr = next_instr_ptr,
79-
None => return Ok(ExecState::Completed),
80+
None => return Ok(self.run_end(chunk_left)),
8081
}
8182
}
83+
chunk_left = CHECKPOINT_INTERVAL;
8284

8385
self.store.execution_fuel = self.store.execution_fuel.saturating_sub(CHECKPOINT_INTERVAL);
8486
if self.store.execution_fuel == 0 {
8587
self.cf.instr_ptr = instr_ptr;
86-
return Ok(ExecState::Suspended(self.cf));
88+
return Ok(RunEnd::State(ExecState::Suspended(self.cf)));
8789
}
8890
}
8991
}
92+
93+
fn run_end(&self, chunk_left: u32) -> RunEnd {
94+
match self.left() {
95+
Some(frame) => RunEnd::Left(frame, chunk_left),
96+
None => RunEnd::State(ExecState::Completed),
97+
}
98+
}
9099
}

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

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ 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+
type UnbudgetedHandler =
7+
for<'store, 'module> fn(&mut Executor<'store, 'module>, &[Instruction], usize, Instruction) -> ExecResult<()>;
8+
type BoundedHandler =
9+
for<'store, 'module> fn(&mut Executor<'store, 'module>, usize, Instruction, u32) -> ExecResult<()>;
810

911
macro_rules! define_unbudgeted_tail_dispatch {
1012
($executor:ident, $instr_ptr:ident, $dispatch_next:ident, $dispatch_flow:ident;
@@ -22,7 +24,7 @@ macro_rules! define_unbudgeted_tail_dispatch {
2224
$(
2325
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
2426
fn $variant(
25-
$executor: &mut Executor<'_>,
27+
$executor: &mut Executor<'_, '_>,
2628
instructions: &[Instruction],
2729
$instr_ptr: usize,
2830
instruction: Instruction,
@@ -45,7 +47,12 @@ macro_rules! define_unbudgeted_tail_dispatch {
4547
$executor.cf.instr_ptr = next_instr_ptr;
4648
return Ok(());
4749
},
48-
ExecFlow::Complete => return cold!({ $executor.completed = true; Ok(()) }),
50+
ExecFlow::Complete => return cold!({
51+
if !$executor.left {
52+
$executor.completed = true;
53+
}
54+
Ok(())
55+
}),
4956
}
5057
}};
5158
}
@@ -79,7 +86,7 @@ macro_rules! define_bounded_tail_dispatch {
7986
$(
8087
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
8188
fn $variant(
82-
$executor: &mut Executor<'_>,
89+
$executor: &mut Executor<'_, '_>,
8390
$instr_ptr: usize,
8491
instruction: Instruction,
8592
instructions_until_checkpoint: u32,
@@ -106,7 +113,11 @@ macro_rules! define_bounded_tail_dispatch {
106113
match $flow.next_instr_ptr() {
107114
Some(next_instr_ptr) => $dispatch_next!(next_instr_ptr),
108115
None => return cold!({
109-
$executor.completed = true;
116+
if $executor.left {
117+
$executor.chunk_left = instructions_until_checkpoint;
118+
} else {
119+
$executor.completed = true;
120+
}
110121
Ok(())
111122
}),
112123
}
@@ -134,13 +145,13 @@ impl Unbudgeted {
134145

135146
#[cold]
136147
#[inline(never)]
137-
fn handler_mismatch(_: &mut Executor<'_>, _: &[Instruction], _: usize, _: Instruction) -> ExecResult<()> {
148+
fn handler_mismatch(_: &mut Executor<'_, '_>, _: &[Instruction], _: usize, _: Instruction) -> ExecResult<()> {
138149
unreachable!("instruction handler mismatch")
139150
}
140151

141152
#[cold]
142153
#[inline(never)]
143-
fn invalid_instr_ptr(_: &mut Executor<'_>, _: &[Instruction], instr_ptr: usize, _: Instruction) -> ExecResult<()> {
154+
fn invalid_instr_ptr(_: &mut Executor<'_, '_>, _: &[Instruction], instr_ptr: usize, _: Instruction) -> ExecResult<()> {
144155
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
145156
}
146157
}
@@ -152,79 +163,92 @@ impl Bounded {
152163

153164
#[cold]
154165
#[inline(never)]
155-
fn handler_mismatch(_: &mut Executor<'_>, _: usize, _: Instruction, _: u32) -> ExecResult<()> {
166+
fn handler_mismatch(_: &mut Executor<'_, '_>, _: usize, _: Instruction, _: u32) -> ExecResult<()> {
156167
unreachable!("instruction handler mismatch")
157168
}
158169

159170
#[cold]
160171
#[inline(never)]
161-
fn invalid_instr_ptr(_: &mut Executor<'_>, instr_ptr: usize, _: Instruction, _: u32) -> ExecResult<()> {
172+
fn invalid_instr_ptr(_: &mut Executor<'_, '_>, instr_ptr: usize, _: Instruction, _: u32) -> ExecResult<()> {
162173
unreachable!("instruction pointer {instr_ptr} out of range, this is a bug")
163174
}
164175

176+
/// Runs up to `chunk_left` (at least 1) instructions from `executor.cf`.
165177
#[inline(always)]
166-
fn run(executor: &mut Executor<'_>) -> ExecResult<()> {
178+
fn run(executor: &mut Executor<'_, '_>, chunk_left: u32) -> ExecResult<()> {
167179
let instr_ptr = executor.cf.instr_ptr;
168180
let instruction = executor.func.instructions[instr_ptr];
169181
let handler = Self::handler_for(instruction.opcode());
170-
handler(executor, instr_ptr, instruction, CHECKPOINT_INTERVAL - 1)
182+
handler(executor, instr_ptr, instruction, chunk_left - 1)
171183
}
172184
}
173185

174-
impl<'store> Executor<'store> {
186+
impl Executor<'_, '_> {
187+
/// Runs until the call completes (`None`) or continues in another module instance's frame.
175188
#[inline(always)]
176-
pub(crate) fn run_to_completion(mut self) -> Result<()> {
189+
pub(crate) fn run_to_completion(mut self) -> Result<Option<CallFrame>> {
177190
loop {
178-
let func = self.func.clone();
191+
let func = self.func;
179192
let instructions = &func.instructions;
180193
let instr_ptr = self.cf.instr_ptr;
181194
let instruction = instructions[instr_ptr];
182195
let handler = Unbudgeted::handler_for(instruction.opcode());
183196
handler(&mut self, instructions, instr_ptr, instruction)?;
184-
if self.completed {
185-
return Ok(());
197+
if self.completed || self.left {
198+
return Ok(self.left());
186199
}
187200
}
188201
}
189202

203+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until `time_budget` has
204+
/// elapsed since `start`.
190205
#[cfg(feature = "std")]
191206
#[inline(always)]
192-
pub(crate) fn run_with_time_budget(mut self, time_budget: core::time::Duration) -> Result<ExecState> {
193-
use crate::std::time::Instant;
194-
195-
if time_budget.is_zero() {
196-
return Ok(ExecState::Suspended(self.cf));
197-
}
198-
let start = Instant::now();
199-
207+
pub(crate) fn run_with_time_budget(
208+
mut self,
209+
start: crate::std::time::Instant,
210+
time_budget: core::time::Duration,
211+
mut chunk_left: u32,
212+
) -> Result<RunEnd> {
200213
loop {
201-
Bounded::run(&mut self)?;
202-
if self.completed {
203-
return cold!(Ok(ExecState::Completed));
214+
if chunk_left != 0 {
215+
Bounded::run(&mut self, chunk_left)?;
216+
if let Some(end) = self.run_end() {
217+
return Ok(end);
218+
}
204219
}
220+
chunk_left = CHECKPOINT_INTERVAL;
205221
if start.elapsed() >= time_budget {
206-
return cold!(Ok(ExecState::Suspended(self.cf)));
222+
return cold!(Ok(RunEnd::State(ExecState::Suspended(self.cf))));
207223
}
208224
}
209225
}
210226

227+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until the store's fuel is out.
211228
#[inline(always)]
212-
pub(crate) fn run_with_fuel(mut self, fuel: u32) -> Result<ExecState> {
229+
pub(crate) fn run_with_fuel(mut self, mut chunk_left: u32) -> Result<RunEnd> {
213230
self.fuel_metered = true;
214-
self.store.execution_fuel = fuel;
215-
if self.store.execution_fuel == 0 {
216-
return Ok(ExecState::Suspended(self.cf));
217-
}
218-
219231
loop {
220-
Bounded::run(&mut self)?;
221-
if self.completed {
222-
return cold!(Ok(ExecState::Completed));
232+
if chunk_left != 0 {
233+
Bounded::run(&mut self, chunk_left)?;
234+
if let Some(end) = self.run_end() {
235+
return Ok(end);
236+
}
223237
}
238+
chunk_left = CHECKPOINT_INTERVAL;
224239
self.store.execution_fuel = self.store.execution_fuel.saturating_sub(CHECKPOINT_INTERVAL);
225240
if self.store.execution_fuel == 0 {
226-
return cold!(Ok(ExecState::Suspended(self.cf)));
241+
return cold!(Ok(RunEnd::State(ExecState::Suspended(self.cf))));
227242
}
228243
}
229244
}
245+
246+
/// How a bounded chain that stopped ended, unless it stopped at a checkpoint.
247+
#[inline(always)]
248+
fn run_end(&self) -> Option<RunEnd> {
249+
if self.completed {
250+
return cold!(Some(RunEnd::State(ExecState::Completed)));
251+
}
252+
self.left().map(|frame| RunEnd::Left(frame, self.chunk_left))
253+
}
230254
}

0 commit comments

Comments
 (0)