Skip to content

Commit 8370a09

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 485c5ca commit 8370a09

7 files changed

Lines changed: 498 additions & 145 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: 59 additions & 35 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
#[cold]
1012
#[inline(never)]
@@ -28,7 +30,7 @@ macro_rules! define_unbudgeted_tail_dispatch {
2830
$(
2931
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
3032
fn $variant(
31-
$executor: &mut Executor<'_>,
33+
$executor: &mut Executor<'_, '_>,
3234
instructions: &[Instruction],
3335
$instr_ptr: usize,
3436
instruction: Instruction,
@@ -49,7 +51,12 @@ macro_rules! define_unbudgeted_tail_dispatch {
4951
$executor.cf.instr_ptr = next_instr_ptr;
5052
return Ok(());
5153
},
52-
ExecFlow::Complete => return cold!({ $executor.completed = true; Ok(()) }),
54+
ExecFlow::Complete => return cold!({
55+
if !$executor.left {
56+
$executor.completed = true;
57+
}
58+
Ok(())
59+
}),
5360
}
5461
}};
5562
}
@@ -83,7 +90,7 @@ macro_rules! define_bounded_tail_dispatch {
8390
$(
8491
#[allow(non_snake_case, unreachable_code, unused_imports, unused_macros, unused_variables)]
8592
fn $variant(
86-
$executor: &mut Executor<'_>,
93+
$executor: &mut Executor<'_, '_>,
8794
$instr_ptr: usize,
8895
instruction: Instruction,
8996
instructions_until_checkpoint: u32,
@@ -108,7 +115,11 @@ macro_rules! define_bounded_tail_dispatch {
108115
match $flow.next_instr_ptr() {
109116
Some(next_instr_ptr) => $dispatch_next!(next_instr_ptr),
110117
None => return cold!({
111-
$executor.completed = true;
118+
if $executor.left {
119+
$executor.chunk_left = instructions_until_checkpoint;
120+
} else {
121+
$executor.completed = true;
122+
}
112123
Ok(())
113124
}),
114125
}
@@ -135,69 +146,82 @@ impl Unbudgeted {
135146
impl Bounded {
136147
instruction_handlers!(define_bounded_tail_dispatch);
137148

149+
/// Runs up to `chunk_left` (at least 1) instructions from `executor.cf`.
138150
#[inline(always)]
139-
fn run(executor: &mut Executor<'_>) -> ExecResult<()> {
151+
fn run(executor: &mut Executor<'_, '_>, chunk_left: u32) -> ExecResult<()> {
140152
let instr_ptr = executor.cf.instr_ptr;
141153
let instruction = executor.func.instructions[instr_ptr];
142154
let handler = Self::handler_for(instruction.opcode());
143-
handler(executor, instr_ptr, instruction, CHECKPOINT_INTERVAL - 1)
155+
handler(executor, instr_ptr, instruction, chunk_left - 1)
144156
}
145157
}
146158

147-
impl<'store> Executor<'store> {
159+
impl Executor<'_, '_> {
160+
/// Runs until the call completes (`None`) or continues in another module instance's frame.
148161
#[inline(always)]
149-
pub(crate) fn run_to_completion(mut self) -> Result<()> {
162+
pub(crate) fn run_to_completion(mut self) -> Result<Option<CallFrame>> {
150163
loop {
151-
let func = self.func.clone();
164+
let func = self.func;
152165
let instructions = &func.instructions;
153166
let instr_ptr = self.cf.instr_ptr;
154167
let instruction = instructions[instr_ptr];
155168
let handler = Unbudgeted::handler_for(instruction.opcode());
156169
handler(&mut self, instructions, instr_ptr, instruction)?;
157-
if self.completed {
158-
return Ok(());
170+
if self.completed || self.left {
171+
return Ok(self.left());
159172
}
160173
}
161174
}
162175

176+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until `time_budget` has
177+
/// elapsed since `start`.
163178
#[cfg(feature = "std")]
164179
#[inline(always)]
165-
pub(crate) fn run_with_time_budget(mut self, time_budget: core::time::Duration) -> Result<ExecState> {
166-
use crate::std::time::Instant;
167-
168-
if time_budget.is_zero() {
169-
return Ok(ExecState::Suspended(self.cf));
170-
}
171-
let start = Instant::now();
172-
180+
pub(crate) fn run_with_time_budget(
181+
mut self,
182+
start: crate::std::time::Instant,
183+
time_budget: core::time::Duration,
184+
mut chunk_left: u32,
185+
) -> Result<RunEnd> {
173186
loop {
174-
Bounded::run(&mut self)?;
175-
if self.completed {
176-
return cold!(Ok(ExecState::Completed));
187+
if chunk_left != 0 {
188+
Bounded::run(&mut self, chunk_left)?;
189+
if let Some(end) = self.run_end() {
190+
return Ok(end);
191+
}
177192
}
193+
chunk_left = CHECKPOINT_INTERVAL;
178194
if start.elapsed() >= time_budget {
179-
return cold!(Ok(ExecState::Suspended(self.cf)));
195+
return cold!(Ok(RunEnd::State(ExecState::Suspended(self.cf))));
180196
}
181197
}
182198
}
183199

200+
/// Runs `chunk_left` instructions, then checkpoint by checkpoint until the store's fuel is out.
184201
#[inline(always)]
185-
pub(crate) fn run_with_fuel(mut self, fuel: u32) -> Result<ExecState> {
202+
pub(crate) fn run_with_fuel(mut self, mut chunk_left: u32) -> Result<RunEnd> {
186203
self.fuel_metered = true;
187-
self.store.execution_fuel = fuel;
188-
if self.store.execution_fuel == 0 {
189-
return Ok(ExecState::Suspended(self.cf));
190-
}
191-
192204
loop {
193-
Bounded::run(&mut self)?;
194-
if self.completed {
195-
return cold!(Ok(ExecState::Completed));
205+
if chunk_left != 0 {
206+
Bounded::run(&mut self, chunk_left)?;
207+
if let Some(end) = self.run_end() {
208+
return Ok(end);
209+
}
196210
}
211+
chunk_left = CHECKPOINT_INTERVAL;
197212
self.store.execution_fuel = self.store.execution_fuel.saturating_sub(CHECKPOINT_INTERVAL);
198213
if self.store.execution_fuel == 0 {
199-
return cold!(Ok(ExecState::Suspended(self.cf)));
214+
return cold!(Ok(RunEnd::State(ExecState::Suspended(self.cf))));
200215
}
201216
}
202217
}
218+
219+
/// How a bounded chain that stopped ended, unless it stopped at a checkpoint.
220+
#[inline(always)]
221+
fn run_end(&self) -> Option<RunEnd> {
222+
if self.completed {
223+
return cold!(Some(RunEnd::State(ExecState::Completed)));
224+
}
225+
self.left().map(|frame| RunEnd::Left(frame, self.chunk_left))
226+
}
203227
}

0 commit comments

Comments
 (0)