Skip to content

Commit 28902aa

Browse files
authored
Simplify Wasmi executor call infrastructure (#2000)
* simplify Wasmi executor call infrastructure * revert changes to replace these caused a performance regression for call-intense workloads. * re-introduce reverted change without perf noise this time benchmarks indicate noise but no regressions
1 parent 000ab27 commit 28902aa

2 files changed

Lines changed: 100 additions & 51 deletions

File tree

crates/wasmi/src/engine/executor/handler/state.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,13 @@ impl Stack {
872872
}
873873

874874
/// Adjusts `self` after returning from a function.
875+
///
876+
/// # Note
877+
///
878+
/// The cached `(memory 0)` is only re-extracted if the returned-to frame uses a
879+
/// different [`Inst`]. This is sound because every operation that can grow a memory
880+
/// refreshes the cache at the growth site instead: the `memory.grow` handler and
881+
/// each of the host call paths.
875882
pub fn pop_frame(
876883
&mut self,
877884
store: &mut PrunedStore,
@@ -1066,12 +1073,7 @@ impl ValueStack {
10661073
let callee_end = callee_start.add(callee_size)?;
10671074
self.grow_if_needed(callee_end)?;
10681075
let caller_sp = self.sp(caller_start);
1069-
let Some(cells) = self.cells_from_to(callee_start, callee_end) else {
1070-
unsafe { unreachable_unchecked!("must fit slice after `grow_if_needed` operation") }
1071-
};
1072-
let Ok(inout) = InOutParams::new(cells, params_len, results_len) else {
1073-
panic!("todo")
1074-
};
1076+
let inout = self.host_inout(callee_start, callee_end, params_len, results_len);
10751077
let control = match caller {
10761078
Some((ip, _, instance)) => ReturnCallHost::Continue((ip, caller_sp, instance)),
10771079
None => ReturnCallHost::Break(caller_sp),
@@ -1094,13 +1096,36 @@ impl ValueStack {
10941096
let callee_end = callee_start.add(callee_size)?;
10951097
self.grow_if_needed(callee_end)?;
10961098
let sp = self.sp(caller_start);
1097-
let Some(cells) = self.cells_from_to(callee_start, callee_end) else {
1099+
let inout = self.host_inout(callee_start, callee_end, params_len, results_len);
1100+
Ok((sp, inout))
1101+
}
1102+
1103+
/// Returns the [`InOutParams`] window `cells[start..end]` for a host function call.
1104+
///
1105+
/// # Note
1106+
///
1107+
/// Both `end - start` and `max(len_params, len_results)` denote the callee size,
1108+
/// and [`ValueStack::grow_if_needed`] has already been called for `end`.
1109+
fn host_inout(
1110+
&mut self,
1111+
start: SpOffset,
1112+
end: SpOffset,
1113+
len_params: usize,
1114+
len_results: usize,
1115+
) -> InOutParams<'_> {
1116+
debug_assert_eq!(
1117+
end.into_inner() - start.into_inner(),
1118+
len_params.max(len_results)
1119+
);
1120+
let Some(cells) = self.cells_from_to(start, end) else {
10981121
unsafe { unreachable_unchecked!("must fit slice after `grow_if_needed` operation") }
10991122
};
1100-
let Ok(inout) = InOutParams::new(cells, params_len, results_len) else {
1101-
panic!("todo")
1123+
let Ok(inout) = InOutParams::new(cells, len_params, len_results) else {
1124+
unsafe {
1125+
unreachable_unchecked!("host frame cells are sized as max(len_params, len_results)")
1126+
}
11021127
};
1103-
Ok((sp, inout))
1128+
inout
11041129
}
11051130

11061131
/// Adjusts `self` for a normal function call.
@@ -1369,9 +1394,11 @@ impl CallStack {
13691394
///
13701395
/// # Note
13711396
///
1372-
/// - If `instance` is `Some` it refers to the _callee_ instance of the tail call
1373-
/// which is required to be different from the currently used instance.
1397+
/// - If `instance` is `Some` it refers to the _callee_ instance of the tail call.
1398+
/// Only an actual instance change creates a restoration obligation.
13741399
/// - If `instance` is `None` the callee shares the currently used instance.
1400+
///
1401+
/// This mirrors the contract of [`CallStack::push`].
13751402
#[inline(always)]
13761403
fn replace(&mut self, callee_ip: Ip, instance: Option<Inst>) -> Result<SpOffset, TrapCode> {
13771404
let Some(caller_frame) = self.frames.last_mut() else {
@@ -1380,12 +1407,11 @@ impl CallStack {
13801407
let start = caller_frame.start;
13811408
caller_frame.ip = callee_ip;
13821409
if let Some(callee_instance) = instance {
1383-
debug_assert!(self.instance != Some(callee_instance));
13841410
// The replaced frame's restoration obligation is carried over unchanged.
13851411
// However, if the replaced frame has no such obligation yet, its caller
13861412
// runs in the currently used instance which must be restored when the
13871413
// new frame returns since the callee continues in a different instance.
1388-
if caller_frame.instance.is_none() {
1414+
if caller_frame.instance.is_none() && self.instance != Some(callee_instance) {
13891415
caller_frame.instance = self.instance;
13901416
}
13911417
self.instance = Some(callee_instance);

crates/wasmi/src/engine/executor/handler/utils.rs

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ use crate::{
2121
ShiftAmount,
2222
},
2323
engine::{
24+
CodeView,
2425
DedupFuncType,
2526
FuncEntry,
27+
InOutParams,
2628
executor::{
2729
LoadFromCellsByValue,
2830
StoreToCells,
2931
handler::{Break, Control, DoneReason, args::Args},
3032
},
3133
utils::unreachable_unchecked,
3234
},
33-
func::{FuncEntity, HostFuncEntity},
35+
func::{FuncEntity, HostFuncEntity, Trampoline},
3436
instance::{DataAddr, ElemAddr, FuncAddr, GlobalAddr, InstanceEntity, MemoryAddr, TableAddr},
3537
ir::{self, Address, BoundedSlotSpan, Local, Offset, Offset16, Slot, SlotAndReg, SlotSpan},
3638
memory::DataSegmentEntity,
@@ -816,6 +818,38 @@ pub fn return_call_func_entry(
816818
Control::Continue((callee_ip, callee_sp))
817819
}
818820

821+
/// Invokes the host function behind `trampoline`.
822+
///
823+
/// Returns the host provided error if the host function trapped.
824+
///
825+
/// # Note
826+
///
827+
/// Takes `store` and `code` instead of the whole [`VmState`] since `inout` already
828+
/// borrows its [`Stack`].
829+
///
830+
/// [`Stack`]: crate::engine::executor::Stack
831+
#[inline]
832+
fn invoke_host(
833+
store: &mut PrunedStore,
834+
code: &mut CodeView,
835+
trampoline: Trampoline,
836+
instance: Option<Inst>,
837+
inout: InOutParams<'_>,
838+
call_hooks: CallHooks,
839+
) -> Result<(), Error> {
840+
match store.call_host_func(trampoline, instance, inout, call_hooks) {
841+
Ok(()) => {}
842+
Err(StoreError::External(error)) => return Err(error),
843+
Err(StoreError::Internal(error)) => unsafe {
844+
unreachable_unchecked!(
845+
"internal interpreter error while executing host function: {error}"
846+
)
847+
},
848+
}
849+
code.refresh();
850+
Ok(())
851+
}
852+
819853
pub fn call_host(
820854
state: &mut VmState,
821855
func: Func,
@@ -831,21 +865,16 @@ pub fn call_host(
831865
.stack
832866
.prepare_host_frame(caller_ip, params, host_func.len_result_cells())
833867
.into_control()?;
834-
match state
835-
.store
836-
.call_host_func(trampoline, instance, inout, call_hooks)
837-
{
838-
Ok(()) => {}
839-
Err(StoreError::External(error)) => {
840-
done!(state, DoneReason::host_error(error, func, params.span()))
841-
}
842-
Err(StoreError::Internal(error)) => unsafe {
843-
unreachable_unchecked!(
844-
"internal interpreter error while executing host function: {error}"
845-
)
846-
},
868+
if let Err(error) = invoke_host(
869+
state.store,
870+
&mut state.code,
871+
trampoline,
872+
instance,
873+
inout,
874+
call_hooks,
875+
) {
876+
done!(state, DoneReason::host_error(error, func, params.span()))
847877
}
848-
state.code.refresh();
849878
Control::Continue(sp)
850879
}
851880

@@ -862,27 +891,22 @@ pub fn return_call_host(
862891
.stack
863892
.return_prepare_host_frame(params, host_func.len_result_cells(), instance)
864893
.into_control()?;
865-
match state
866-
.store
867-
.call_host_func(trampoline, Some(instance), inout, CallHooks::Call)
868-
{
869-
Ok(()) => {}
870-
Err(StoreError::External(error)) => {
871-
// Note: we won't allow resumption in case the execution would
872-
// have returned with this the host function tail call.
873-
let reason = match control {
874-
Control::Continue(_) => DoneReason::host_error(error, func, params.span()),
875-
Control::Break(_) => DoneReason::error(error),
876-
};
877-
done!(state, reason)
878-
}
879-
Err(StoreError::Internal(error)) => unsafe {
880-
unreachable_unchecked!(
881-
"internal interpreter error while executing host function: {error}"
882-
)
883-
},
894+
if let Err(error) = invoke_host(
895+
state.store,
896+
&mut state.code,
897+
trampoline,
898+
Some(instance),
899+
inout,
900+
CallHooks::Call,
901+
) {
902+
// Note: we won't allow resumption in case the execution would
903+
// have returned with this the host function tail call.
904+
let reason = match control {
905+
Control::Continue(_) => DoneReason::host_error(error, func, params.span()),
906+
Control::Break(_) => DoneReason::error(error),
907+
};
908+
done!(state, reason)
884909
}
885-
state.code.refresh();
886910
match control {
887911
Control::Continue((ip, sp, instance)) => Control::Continue((ip, sp, instance)),
888912
Control::Break(sp) => done!(state, DoneReason::Return(sp)),
@@ -965,9 +989,8 @@ pub fn return_call_wasm_or_host(
965989
};
966990
// Hot path: tail-calling a Wasm function. See `call_wasm_or_host` for the shape.
967991
let callee_instance: Inst = resolve_instance(state.store, wasm_func.instance()).into();
968-
let changed_instance = (callee_instance != instance).then_some(callee_instance);
969992
let (callee_ip, callee_sp) =
970-
return_call_func_entry(state, params, wasm_func.func_entry(), changed_instance)?;
993+
return_call_func_entry(state, params, wasm_func.func_entry(), Some(callee_instance))?;
971994
let (instance, mem0, mem0_len) =
972995
update_instance(state.store, instance, callee_instance, mem0, mem0_len);
973996
Control::Continue((callee_ip, callee_sp, mem0, mem0_len, instance))

0 commit comments

Comments
 (0)