Skip to content

Commit 461de46

Browse files
authored
Cache FuncEntry in FuncEntity and make use of this caching in the executor (#1996)
* add FuncEntry to FuncEntity and use it in executor * fix performance call_internal regressions * fix stackoverflow while retaining performance wins * fix broken intra doc links
1 parent 3af6b1c commit 461de46

7 files changed

Lines changed: 119 additions & 122 deletions

File tree

crates/wasmi/src/engine/code_map/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,49 @@ pub struct FuncEntry {
550550
state: AtomicU8,
551551
}
552552

553+
/// A stable address of a [`FuncEntry`] in the engine's append-only [`CodeMap`].
554+
///
555+
/// Holding this instead of an `EngineFunc` lets callers reach the [`FuncEntry`] directly, skipping
556+
/// the [`CodeMap`] lookup. It stores the address as an exposed-provenance `usize` (the same
557+
/// convention the `call_internal` handler uses for its baked pointer) which keeps it `Send + Sync`
558+
/// without an `unsafe impl`.
559+
///
560+
/// [`CodeMap`]: crate::engine::CodeMap
561+
#[derive(Debug, Copy, Clone)]
562+
pub struct FuncEntryPtr(usize);
563+
564+
impl From<usize> for FuncEntryPtr {
565+
/// Creates a [`FuncEntryPtr`] from a previously exposed [`FuncEntry`] address.
566+
///
567+
/// Used by the `call_internal` handlers to recover the address baked into the bytecode.
568+
fn from(addr: usize) -> Self {
569+
Self(addr)
570+
}
571+
}
572+
573+
impl FuncEntryPtr {
574+
/// Creates a [`FuncEntryPtr`] from `entry`, exposing its provenance.
575+
pub fn new(entry: &FuncEntry) -> Self {
576+
Self(ptr::from_ref(entry).expose_provenance())
577+
}
578+
579+
/// Returns a shared reference to the pointed-to [`FuncEntry`].
580+
///
581+
/// # Safety
582+
///
583+
/// The engine owning the [`FuncEntry`] must outlive `'a`. This holds for a [`FuncEntryPtr`]
584+
/// stored in a [`WasmFuncEntity`] since its [`Func`] cannot outlive the owning engine. The
585+
/// [`FuncEntry`] is only ever accessed through shared references (interior mutation is guarded
586+
/// by atomics), so no aliasing `&mut` is ever formed.
587+
///
588+
/// [`WasmFuncEntity`]: crate::func::WasmFuncEntity
589+
/// [`Func`]: crate::Func
590+
pub unsafe fn get<'a>(self) -> &'a FuncEntry {
591+
// SAFETY: guaranteed by the caller (see above); provenance was exposed in `new`.
592+
unsafe { &*ptr::with_exposed_provenance::<FuncEntry>(self.0) }
593+
}
594+
}
595+
553596
impl Drop for FuncEntry {
554597
fn drop(&mut self) {
555598
match self.state.load(Ordering::Acquire) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ impl Args {
221221
}
222222

223223
/// Calls `func` with `params` on `instance` with `state` using `self`.
224-
#[inline]
224+
#[inline(always)]
225225
pub fn call_func_entry(
226226
&mut self,
227227
state: &mut VmState,
@@ -234,7 +234,7 @@ impl Args {
234234
}
235235

236236
/// Tail-calls `func` with `params` on `instance` with `state` using `self`.
237-
#[inline]
237+
#[inline(always)]
238238
pub fn return_call_func_entry(
239239
&mut self,
240240
state: &mut VmState,

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

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
TrapCode,
2020
core::{CoreTable, RawRef, ReadAs, WriteAs, wasm},
2121
engine::{
22-
FuncEntry,
22+
FuncEntryPtr,
2323
eval,
2424
executor::handler::{
2525
Control,
@@ -33,7 +33,7 @@ use crate::{
3333
ir::{self, BoundedSlotSpan},
3434
store::StoreError,
3535
};
36-
use core::{cmp, ptr};
36+
use core::cmp;
3737

3838
fn identity<T>(value: T) -> T {
3939
value
@@ -205,14 +205,9 @@ execution_handler! {
205205
) -> Done = {
206206
let mut args = Args::from_parts(ip, sp, mem0, mem0_len, instance, ireg, freg32, freg64);
207207
let crate::ir::decode::CallInternal { params, func } = unsafe { args.decode_op() };
208-
// Safety:
209-
//
210-
// `func` is the exposed address of a `FuncEntity` in the engine's append-only `CodeMap`.
211-
//
212-
// - the used `with_exposed_provenance` recovers the original provenance.
213-
// - its allocation is never moved or freed while this bytecode runs.
214-
// - the `FuncEntry` type mutates only guarded by lock-free atomics.
215-
let func = unsafe { &*ptr::with_exposed_provenance::<FuncEntry>(usize::from(func)) };
208+
// SAFETY: `func` is the exposed address of a `FuncEntry` in the engine's append-only
209+
// `CodeMap`, baked into the bytecode; it stays valid while this bytecode runs.
210+
let func = unsafe { FuncEntryPtr::from(usize::from(func)).get() };
216211
args.call_func_entry(state, func, params, None)?;
217212
dispatch!(state, args)
218213
}
@@ -287,14 +282,8 @@ execution_handler! {
287282
) -> Done = {
288283
let mut args = Args::from_parts(ip, sp, mem0, mem0_len, instance, ireg, freg32, freg64);
289284
let crate::ir::decode::ReturnCallInternal { params, func } = unsafe { args.decode_op() };
290-
// Safety:
291-
//
292-
// `func` is the exposed address of a `FuncEntity` in the engine's append-only `CodeMap`.
293-
//
294-
// - the used `with_exposed_provenance` recovers the original provenance.
295-
// - its allocation is never moved or freed while this bytecode runs.
296-
// - the `FuncEntry` type mutates only guarded by lock-free atomics.
297-
let func = unsafe { &*ptr::with_exposed_provenance::<FuncEntry>(usize::from(func)) };
285+
// SAFETY: see `call_internal`.
286+
let func = unsafe { FuncEntryPtr::from(usize::from(func)).get() };
298287
args.return_call_func_entry(state, func, params, None)?;
299288
dispatch!(state, args)
300289
}

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

Lines changed: 37 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ use crate::{
2222
},
2323
engine::{
2424
DedupFuncType,
25-
EngineFunc,
2625
FuncEntry,
2726
executor::{
2827
LoadFromCellsByValue,
@@ -85,22 +84,6 @@ macro_rules! compile_or_get_func_entry {
8584
}};
8685
}
8786

88-
pub fn compile_or_get_func(state: &mut VmState, func: EngineFunc) -> Result<(Ip, u16, u16), Error> {
89-
let Some(func_entry) = state.code.entry(func) else {
90-
unreachable!("missing function entry at: {func:?}")
91-
};
92-
compile_or_get_func_entry(state, func_entry)
93-
}
94-
95-
macro_rules! compile_or_get_func {
96-
($state:expr, $func:expr) => {{
97-
match $crate::engine::executor::handler::utils::compile_or_get_func($state, $func) {
98-
Ok((ip, len_local_slots, len_stack_slots)) => (ip, len_local_slots, len_stack_slots),
99-
Err(error) => done!($state, DoneReason::error(error)),
100-
}
101-
}};
102-
}
103-
10487
macro_rules! trap {
10588
($trap_code:expr) => {{
10689
return $crate::engine::executor::handler::Control::Break(
@@ -789,7 +772,7 @@ pub fn update_instance(
789772
(new_instance, mem0, mem0_len)
790773
}
791774

792-
#[inline]
775+
#[inline(always)]
793776
pub fn call_func_entry(
794777
state: &mut VmState,
795778
caller_ip: Ip,
@@ -812,30 +795,7 @@ pub fn call_func_entry(
812795
Control::Continue((callee_ip, callee_sp))
813796
}
814797

815-
#[inline(never)]
816-
pub fn call_wasm(
817-
state: &mut VmState,
818-
caller_ip: Ip,
819-
params: BoundedSlotSpan,
820-
func: EngineFunc,
821-
instance: Option<Inst>,
822-
) -> Control<(Ip, Sp), Break> {
823-
let (callee_ip, len_local_slots, len_stack_slots) = compile_or_get_func!(state, func);
824-
let callee_sp = state
825-
.stack
826-
.push_frame(
827-
Some(caller_ip),
828-
callee_ip,
829-
params,
830-
len_local_slots,
831-
len_stack_slots,
832-
instance,
833-
)
834-
.into_control()?;
835-
Control::Continue((callee_ip, callee_sp))
836-
}
837-
838-
#[inline]
798+
#[inline(always)]
839799
pub fn return_call_func_entry(
840800
state: &mut VmState,
841801
params: BoundedSlotSpan,
@@ -856,27 +816,6 @@ pub fn return_call_func_entry(
856816
Control::Continue((callee_ip, callee_sp))
857817
}
858818

859-
#[inline(never)]
860-
pub fn return_call_wasm(
861-
state: &mut VmState,
862-
params: BoundedSlotSpan,
863-
func: EngineFunc,
864-
instance: Option<Inst>,
865-
) -> Control<(Ip, Sp), Break> {
866-
let (callee_ip, len_local_slots, len_stack_slots) = compile_or_get_func!(state, func);
867-
let callee_sp = state
868-
.stack
869-
.replace_frame(
870-
callee_ip,
871-
params,
872-
len_local_slots,
873-
len_stack_slots,
874-
instance,
875-
)
876-
.into_control()?;
877-
Control::Continue((callee_ip, callee_sp))
878-
}
879-
880819
pub fn call_host(
881820
state: &mut VmState,
882821
func: Func,
@@ -950,6 +889,7 @@ pub fn return_call_host(
950889
}
951890
}
952891

892+
#[inline]
953893
#[expect(clippy::too_many_arguments)]
954894
pub fn call_wasm_or_host(
955895
state: &mut VmState,
@@ -965,39 +905,41 @@ pub fn call_wasm_or_host(
965905
// (indirect calls); the reference is only used to copy out the callee data below,
966906
// before any store mutation, so it never aliases a `&mut` into the funcs arena.
967907
let func_entity = unsafe { func_entity.as_ref() };
968-
let next_state = match func_entity {
969-
FuncEntity::Wasm(wasm_func) => {
970-
let func = wasm_func.func_body();
971-
let callee_instance = *wasm_func.instance();
972-
let callee_instance: Inst = resolve_instance(state.store, &callee_instance).into();
973-
let (callee_ip, callee_sp) =
974-
call_wasm(state, caller_ip, params, func, Some(callee_instance))?;
975-
let (instance, mem0, mem0_len) =
976-
update_instance(state.store, instance, callee_instance, mem0, mem0_len);
977-
(callee_ip, callee_sp, mem0, mem0_len, instance)
978-
}
908+
let wasm_func = match func_entity {
909+
FuncEntity::Wasm(wasm_func) => wasm_func,
979910
FuncEntity::Host(host_func) => {
980-
let host_func = *host_func;
981911
let sp = call_host(
982912
state,
983913
func,
984914
Some(caller_ip),
985-
host_func,
915+
*host_func,
986916
params,
987917
Some(instance),
988918
CallHooks::Call,
989919
)?;
990-
// Host functions may re-enter WASM (e.g. calling cabi_realloc)
991-
// which can trigger memory.grow, invalidating the cached mem0
992-
// pointer. Re-extract to avoid stale pointer dereference.
920+
// Note: host functions may grow memories, invalidating the cached `(memory 0)`.
921+
// Therefore, it is required to re-extract `(memory 0)` to avoid a stale cache.
993922
let (mem0, mem0_len) = extract_mem0(state.store, instance);
994-
(caller_ip, sp, mem0, mem0_len, instance)
923+
return Control::Continue((caller_ip, sp, mem0, mem0_len, instance));
995924
}
996925
};
997-
Control::Continue(next_state)
926+
// Hot path: calling a Wasm function. Uses the cached `FuncEntry` and the same inlined
927+
// machinery as `call_internal`, differing only in the possible instance switch.
928+
let callee_instance: Inst = resolve_instance(state.store, wasm_func.instance()).into();
929+
let (callee_ip, callee_sp) = call_func_entry(
930+
state,
931+
caller_ip,
932+
params,
933+
wasm_func.func_entry(),
934+
Some(callee_instance),
935+
)?;
936+
let (instance, mem0, mem0_len) =
937+
update_instance(state.store, instance, callee_instance, mem0, mem0_len);
938+
Control::Continue((callee_ip, callee_sp, mem0, mem0_len, instance))
998939
}
999940

1000941
/// Tail-call (`return_call`) twin of [`call_wasm_or_host`].
942+
#[inline]
1001943
pub fn return_call_wasm_or_host(
1002944
state: &mut VmState,
1003945
func: Func,
@@ -1009,25 +951,22 @@ pub fn return_call_wasm_or_host(
1009951
) -> Control<(Ip, Sp, Mem0Ptr, Mem0Len, Inst), Break> {
1010952
// SAFETY: see `call_wasm_or_host`.
1011953
let func_entity = unsafe { func_entity.as_ref() };
1012-
let (callee_ip, sp, new_instance) = match func_entity {
1013-
FuncEntity::Wasm(wasm_func) => {
1014-
let wasm_func_body = wasm_func.func_body();
1015-
let callee_instance = *wasm_func.instance();
1016-
let callee_instance: Inst = resolve_instance(state.store, &callee_instance).into();
1017-
let changed_instance = match callee_instance != instance {
1018-
true => Some(callee_instance),
1019-
false => None,
1020-
};
1021-
let (callee_ip, callee_sp) =
1022-
return_call_wasm(state, params, wasm_func_body, changed_instance)?;
1023-
(callee_ip, callee_sp, callee_instance)
1024-
}
954+
let wasm_func = match func_entity {
955+
FuncEntity::Wasm(wasm_func) => wasm_func,
1025956
FuncEntity::Host(host_func) => {
1026-
let host_func = *host_func;
1027-
return_call_host(state, func, host_func, params, instance)?
957+
let (callee_ip, sp, new_instance) =
958+
return_call_host(state, func, *host_func, params, instance)?;
959+
let (instance, mem0, mem0_len) =
960+
update_instance(state.store, instance, new_instance, mem0, mem0_len);
961+
return Control::Continue((callee_ip, sp, mem0, mem0_len, instance));
1028962
}
1029963
};
964+
// Hot path: tail-calling a Wasm function. See `call_wasm_or_host` for the shape.
965+
let callee_instance: Inst = resolve_instance(state.store, wasm_func.instance()).into();
966+
let changed_instance = (callee_instance != instance).then_some(callee_instance);
967+
let (callee_ip, callee_sp) =
968+
return_call_func_entry(state, params, wasm_func.func_entry(), changed_instance)?;
1030969
let (instance, mem0, mem0_len) =
1031-
update_instance(state.store, instance, new_instance, mem0, mem0_len);
1032-
Control::Continue((callee_ip, sp, mem0, mem0_len, instance))
970+
update_instance(state.store, instance, callee_instance, mem0, mem0_len);
971+
Control::Continue((callee_ip, callee_sp, mem0, mem0_len, instance))
1033972
}

crates/wasmi/src/engine/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod utils;
1515
pub(crate) use self::translator::ValidatingFuncTranslator;
1616
pub(crate) use self::{
1717
block_type::BlockType,
18+
code_map::{FuncEntry, FuncEntryPtr},
1819
executor::{
1920
InOutParams,
2021
InOutResults,
@@ -61,7 +62,6 @@ use crate::{
6162
Func,
6263
FuncType,
6364
StoreContextMut,
64-
engine::code_map::FuncEntry,
6565
module::{FuncIdx, ModuleHeader},
6666
};
6767
use alloc::{

0 commit comments

Comments
 (0)