Skip to content

Commit 3e11936

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 95281eb commit 3e11936

7 files changed

Lines changed: 481 additions & 137 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: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ use super::*;
33
struct Unbudgeted;
44
struct Bounded;
55

6-
type UnbudgetedHandler = for<'store> fn(&mut Executor<'store>, usize, Instruction) -> ExecResult<()>;
7-
type BoundedHandler = for<'store> fn(&mut Executor<'store>, usize, Instruction, u32) -> ExecResult<()>;
6+
type UnbudgetedHandler = for<'store, 'module> fn(&mut Executor<'store, 'module>, usize, Instruction) -> ExecResult<()>;
7+
type BoundedHandler =
8+
for<'store, 'module> fn(&mut Executor<'store, 'module>, usize, Instruction, u32) -> ExecResult<()>;
89

910
#[cold]
1011
#[inline(never)]
@@ -28,7 +29,7 @@ macro_rules! define_unbudgeted_tail_dispatch {
2829
$(
2930
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
3031
fn $variant(
31-
$executor: &mut Executor<'_>,
32+
$executor: &mut Executor<'_, '_>,
3233
$instr_ptr: usize,
3334
instruction: Instruction,
3435
) -> ExecResult<()> {
@@ -78,7 +79,7 @@ macro_rules! define_bounded_tail_dispatch {
7879
$(
7980
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
8081
fn $variant(
81-
$executor: &mut Executor<'_>,
82+
$executor: &mut Executor<'_, '_>,
8283
$instr_ptr: usize,
8384
instruction: Instruction,
8485
instructions_until_checkpoint: u32,
@@ -103,7 +104,11 @@ macro_rules! define_bounded_tail_dispatch {
103104
match $flow.next_instr_ptr() {
104105
Some(next_instr_ptr) => $dispatch_next!(next_instr_ptr),
105106
None => return cold!({
106-
$executor.completed = true;
107+
if $executor.left {
108+
$executor.chunk_left = instructions_until_checkpoint;
109+
} else {
110+
$executor.completed = true;
111+
}
107112
Ok(())
108113
}),
109114
}
@@ -130,62 +135,76 @@ impl Unbudgeted {
130135
impl Bounded {
131136
instruction_handlers!(define_bounded_tail_dispatch);
132137

138+
/// Runs up to `chunk_left` (at least 1) instructions from `executor.cf`.
133139
#[inline(always)]
134-
fn run(executor: &mut Executor<'_>) -> ExecResult<()> {
140+
fn run(executor: &mut Executor<'_, '_>, chunk_left: u32) -> ExecResult<()> {
135141
let instr_ptr = executor.cf.instr_ptr;
136142
let instruction = executor.func.instructions[instr_ptr];
137143
let handler = Self::handler_for(instruction.opcode());
138-
handler(executor, instr_ptr, instruction, CHECKPOINT_INTERVAL - 1)
144+
handler(executor, instr_ptr, instruction, chunk_left - 1)
139145
}
140146
}
141147

142-
impl<'store> Executor<'store> {
148+
impl Executor<'_, '_> {
149+
/// Runs until the call completes (`None`) or continues in another module instance's frame.
143150
#[inline(always)]
144-
pub(crate) fn run_to_completion(mut self) -> Result<()> {
151+
pub(crate) fn run_to_completion(mut self) -> Result<Option<CallFrame>> {
145152
let instr_ptr = self.cf.instr_ptr;
146153
let instruction = self.func.instructions[instr_ptr];
147154
let handler = Unbudgeted::handler_for(instruction.opcode());
148-
Ok(handler(&mut self, instr_ptr, instruction)?)
155+
handler(&mut self, instr_ptr, instruction)?;
156+
Ok(self.left())
149157
}
150158

159+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until `time_budget` has
160+
/// elapsed since `start`.
151161
#[cfg(feature = "std")]
152162
#[inline(always)]
153-
pub(crate) fn run_with_time_budget(mut self, time_budget: core::time::Duration) -> Result<ExecState> {
154-
use crate::std::time::Instant;
155-
156-
if time_budget.is_zero() {
157-
return Ok(ExecState::Suspended(self.cf));
158-
}
159-
let start = Instant::now();
160-
163+
pub(crate) fn run_with_time_budget(
164+
mut self,
165+
start: crate::std::time::Instant,
166+
time_budget: core::time::Duration,
167+
mut chunk_left: u32,
168+
) -> Result<RunEnd> {
161169
loop {
162-
Bounded::run(&mut self)?;
163-
if self.completed {
164-
return cold!(Ok(ExecState::Completed));
170+
if chunk_left != 0 {
171+
Bounded::run(&mut self, chunk_left)?;
172+
if let Some(end) = self.run_end() {
173+
return Ok(end);
174+
}
165175
}
176+
chunk_left = CHECKPOINT_INTERVAL;
166177
if start.elapsed() >= time_budget {
167-
return cold!(Ok(ExecState::Suspended(self.cf)));
178+
return cold!(Ok(RunEnd::State(ExecState::Suspended(self.cf))));
168179
}
169180
}
170181
}
171182

183+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until the store's fuel is out.
172184
#[inline(always)]
173-
pub(crate) fn run_with_fuel(mut self, fuel: u32) -> Result<ExecState> {
185+
pub(crate) fn run_with_fuel(mut self, mut chunk_left: u32) -> Result<RunEnd> {
174186
self.fuel_metered = true;
175-
self.store.execution_fuel = fuel;
176-
if self.store.execution_fuel == 0 {
177-
return Ok(ExecState::Suspended(self.cf));
178-
}
179-
180187
loop {
181-
Bounded::run(&mut self)?;
182-
if self.completed {
183-
return cold!(Ok(ExecState::Completed));
188+
if chunk_left != 0 {
189+
Bounded::run(&mut self, chunk_left)?;
190+
if let Some(end) = self.run_end() {
191+
return Ok(end);
192+
}
184193
}
194+
chunk_left = CHECKPOINT_INTERVAL;
185195
self.store.execution_fuel = self.store.execution_fuel.saturating_sub(CHECKPOINT_INTERVAL);
186196
if self.store.execution_fuel == 0 {
187-
return cold!(Ok(ExecState::Suspended(self.cf)));
197+
return cold!(Ok(RunEnd::State(ExecState::Suspended(self.cf))));
188198
}
189199
}
190200
}
201+
202+
/// How a bounded chain that stopped ended, unless it stopped at a checkpoint.
203+
#[inline(always)]
204+
fn run_end(&self) -> Option<RunEnd> {
205+
if self.completed {
206+
return cold!(Some(RunEnd::State(ExecState::Completed)));
207+
}
208+
self.left().map(|frame| RunEnd::Left(frame, self.chunk_left))
209+
}
191210
}

0 commit comments

Comments
 (0)