From 21c36782314056f26054d7c65722620715b7071b Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Fri, 21 Aug 2026 18:30:14 +0000 Subject: [PATCH 1/2] add no-concurrent option to call benchmarks --- benches/call.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/benches/call.rs b/benches/call.rs index 7e35b7fe422d..b48448273c47 100644 --- a/benches/call.rs +++ b/benches/call.rs @@ -45,12 +45,15 @@ impl IsAsync { } } -fn engines() -> Vec<(Engine, IsAsync)> { +fn engines(concurrency_support: bool) -> Vec<(Engine, IsAsync)> { let mut config = Config::new(); #[cfg(feature = "component-model")] config.wasm_component_model(true); + #[cfg(feature = "component-model-async")] + config.concurrency_support(concurrency_support); + let mut pool = PoolingAllocationConfig::default(); if std::env::var("WASMTIME_TEST_FORCE_MPK").is_ok() { pool.memory_protection_keys(Enabled::Yes); @@ -79,7 +82,7 @@ fn engines() -> Vec<(Engine, IsAsync)> { /// Benchmarks the overhead of calling WebAssembly from the host in various /// configurations. fn host_to_wasm(c: &mut Criterion) { - for (engine, is_async) in engines() { + for (engine, is_async) in engines(false) { let mut store = Store::new(&engine, ()); let module = Module::new( &engine, @@ -249,7 +252,7 @@ fn wasm_to_host(c: &mut Criterion) { )"#; - for (engine, is_async) in engines() { + for (engine, is_async) in engines(false) { let mut store = Store::new(&engine, ()); let module = Module::new(&engine, module).unwrap(); @@ -548,8 +551,22 @@ mod component { tuples!(A B); tuples!(A B C); + fn engines() -> Vec<(String, Engine, IsAsync)> { + let mut result: Vec<_> = super::engines(false) + .into_iter() + .map(|(e, a)| ("no-concurrent".to_string(), e, a)) + .collect(); + #[cfg(feature = "component-model-async")] + result.extend( + super::engines(true) + .into_iter() + .map(|(e, a)| ("concurrent".to_string(), e, a)), + ); + result + } + fn host_to_wasm(c: &mut Criterion) { - for (engine, is_async) in engines() { + for (concurrent, engine, is_async) in engines() { let mut store = Store::new(&engine, ()); let component = Component::new( @@ -599,12 +616,12 @@ mod component { }; // Bench once without any call hooks configured - let name = format!("{}/no-hook", is_async.desc()); + let name = format!("{}/{}/no-hook", concurrent, is_async.desc()); bench_calls(&mut c.benchmark_group(&name), &mut store); // Bench again with a "call hook" enabled store.call_hook(|_, _| Ok(())); - let name = format!("{}/hook-sync", is_async.desc()); + let name = format!("{}/{}/hook-sync", concurrent, is_async.desc()); bench_calls(&mut c.benchmark_group(&name), &mut store); } } @@ -738,19 +755,19 @@ mod component { ) "#; - for (engine, is_async) in engines() { + for (concurrent, engine, is_async) in engines() { let mut store = Store::new(&engine, ()); let component = component::Component::new(&engine, module).unwrap(); bench_calls( - &mut c.benchmark_group(&format!("{}/no-hook", is_async.desc())), + &mut c.benchmark_group(&format!("{}/{}/no-hook", concurrent, is_async.desc())), &mut store, &component, is_async, ); store.call_hook(|_, _| Ok(())); bench_calls( - &mut c.benchmark_group(&format!("{}/hook-sync", is_async.desc())), + &mut c.benchmark_group(&format!("{}/{}/hook-sync", concurrent, is_async.desc())), &mut store, &component, is_async, From bf43db0e5caa2a0ddb8e13c88b4be6f95b68a720 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Fri, 21 Aug 2026 13:57:47 +0000 Subject: [PATCH 2/2] wasmtime: Cache `VMFuncRef` in `component::Func` A core `Func` caches a raw pointer to its `VMFuncRef`, but a `component::Func` rederives the pointer on ever call and this contributes to host -> wasm component function calls having significantly higher overhead than core function calls (even concurrency support disabled). This PR caches the `VMFuncRef` for `component::Func` in the same it is currently done for core `Func`. The `VMFuncRef` for the associated `post_return` call is also cached along with Some additional metadata. These are my benchmark results for the impact on nop calls with no arguments or return values: Before change: | Call type | Latency | ----------------------------------------------- | core | 35 ns | | component (concurrency disabled) | 300 ns | | component (concurrency enabled) | 800 ns | After change: | Call type | Latency | ----------------------------------------------- | core | 35 ns | | component (concurrency disabled) | 140 ns | | component (concurrency enabled) | 600 ns | The bencmarks run are: ``` cargo bench --bench call -- --exact "sync/no-hook/core - host-to-wasm - typed - nop" cargo bench --bench call -- --exact "no-concurrent/sync/no-hook/component - host-to-wasm - typed - nop" cargo bench --bench call -- --exact "concurrent/sync/no-hook/component - host-to-wasm - typed - nop" ``` --- .../c-api/include/wasmtime/component/func.h | 15 +++ .../src/runtime/component/concurrent/func.rs | 4 +- crates/wasmtime/src/runtime/component/func.rs | 125 +++++++++++------- .../src/runtime/component/func/typed.rs | 2 +- .../src/runtime/component/instance.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 2 +- 6 files changed, 100 insertions(+), 50 deletions(-) diff --git a/crates/c-api/include/wasmtime/component/func.h b/crates/c-api/include/wasmtime/component/func.h index 2811610e4416..9c5efc987e07 100644 --- a/crates/c-api/include/wasmtime/component/func.h +++ b/crates/c-api/include/wasmtime/component/func.h @@ -33,6 +33,21 @@ typedef struct wasmtime_component_func { /// Private internal wasmtime information. uint32_t __private2; + + /// Private internal wasmtime information. + uint32_t __private3; + + /// Private internal wasmtime information. + uint32_t __private4; + + /// Private internal wasmtime information. + uint8_t __private5; + + /// Private internal wasmtime information. + void *__private6; + + /// Private internal wasmtime information. + void *__private7; } wasmtime_component_func_t; /// \brief Returns the type of this function. diff --git a/crates/wasmtime/src/runtime/component/concurrent/func.rs b/crates/wasmtime/src/runtime/component/concurrent/func.rs index 371bb8e621e9..3936d7d66086 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/func.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/func.rs @@ -183,7 +183,7 @@ impl Func { }) }, move |func, store, results| { - let max_flat = if func.abi_async(store) { + let max_flat = if func.abi_async() { MAX_FLAT_PARAMS } else { MAX_FLAT_RESULTS @@ -431,7 +431,7 @@ where } else { 1 }; - let max_results = if self.func().abi_async(store.0) { + let max_results = if self.func().abi_async() { MAX_FLAT_PARAMS } else { MAX_FLAT_RESULTS diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 5c7f07ba2db9..fe26b5214769 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -5,7 +5,7 @@ use crate::component::types::ComponentFunc; use crate::component::values::Val; use crate::prelude::*; use crate::runtime::vm::component::{ComponentInstance, InstanceFlags}; -use crate::runtime::vm::{Export, VMFuncRef}; +use crate::runtime::vm::{Export, SendSyncPtr, VMFuncRef}; use crate::store::StoreOpaque; use crate::{AsContext, AsContextMut, StoreContextMut, ValRaw}; use core::mem::{self, MaybeUninit}; @@ -33,24 +33,80 @@ pub use self::typed::*; #[repr(C)] // here for the C API. pub struct Func { instance: Instance, - index: ExportIndex, + + /// The component type index of this lifted function. + ty: TypeFuncIndex, + + /// The index of the canonical `options` for this lifted function. + options: OptionsIndex, + + /// Whether this lifted function uses the async canonical ABI. + abi_async: bool, + + /// The resolved core `VMFuncRef` for this lifted function, whose lifetime + /// is bound to the `Store` this `Func` belongs to. + /// + /// Note that this field has an `unsafe_*` prefix to discourage use of it. + /// This is only safe to read/use if the store that owns `instance` + /// (identified by `instance.id().store_id()`) is in scope. Use the + /// `self.lifted_core_func()` method instead of this field to perform this + /// check. + unsafe_func_ref: SendSyncPtr, + + /// The resolved core `VMFuncRef` for this function's `post-return`, if any. + /// + /// Same store-lifetime rules as `unsafe_func_ref`: only valid to read while + /// the owning store is in scope. Read via [`Func::post_return_core_func`], + /// which validates the store id first. + post_return_func_ref: Option>, } -// Double-check that the C representation in `component/instance.h` matches our +// Double-check that the C representation in `component/func.h` matches our // in-Rust representation here in terms of size/alignment/etc. const _: () = { #[repr(C)] struct T(u64, u32); #[repr(C)] - struct C(T, u32); + struct C(T, u32, u32, u32, bool, *mut u8, *mut u8); assert!(core::mem::size_of::() == core::mem::size_of::()); assert!(core::mem::align_of::() == core::mem::align_of::()); assert!(core::mem::offset_of!(Func, instance) == 0); }; impl Func { - pub(crate) fn from_lifted_func(instance: Instance, index: ExportIndex) -> Func { - Func { instance, index } + pub(crate) fn from_lifted_func( + store: &mut StoreOpaque, + instance: Instance, + index: ExportIndex, + ) -> Func { + let def = { + let vminstance = instance.id().get(store); + let (_ty, def, _options) = vminstance.component().export_lifted_function(index); + def.clone() + }; + let unsafe_func_ref = match instance.lookup_vmdef(store, &def) { + Export::Function(f) => f.vm_func_ref(store), + _ => unreachable!(), + } + .into(); + + let vminstance = instance.id().get(store); + let component = vminstance.component(); + let (ty, _def, options) = component.export_lifted_function(index); + let raw_options = &component.env_component().options[options]; + let abi_async = raw_options.async_; + let post_return_func_ref = raw_options + .post_return + .map(|i| SendSyncPtr::from(vminstance.runtime_post_return(i))); + + Func { + instance, + ty, + options, + abi_async, + unsafe_func_ref, + post_return_func_ref, + } } /// Attempt to cast this [`Func`] to a statically typed [`TypedFunc`] with @@ -167,7 +223,7 @@ impl Func { Return: ComponentNamedList + Lift, { let cx = InstanceType::new(instance.unwrap_or_else(|| self.instance.id().get(store))); - let ty = &cx.types[self.ty_index(store)]; + let ty = &cx.types[self.ty]; Params::typecheck(&InterfaceType::Tuple(ty.params), &cx) .context("type mismatch with parameters")?; @@ -184,14 +240,7 @@ impl Func { fn ty_(&self, store: &StoreOpaque) -> ComponentFunc { let cx = InstanceType::new(self.instance.id().get(store)); - let ty = self.ty_index(store); - ComponentFunc::from(ty, &cx) - } - - fn ty_index(&self, store: &StoreOpaque) -> TypeFuncIndex { - let instance = self.instance.id().get(store); - let (ty, _, _) = instance.component().export_lifted_function(self.index); - ty + ComponentFunc::from(self.ty, &cx) } /// Invokes this function with the `params` given and returns the result. @@ -313,7 +362,7 @@ impl Func { self.check_params_results(store.as_context_mut(), params, results)?; - if self.abi_async(store.0) { + if self.abi_async() { unreachable!( "async-lifted exports should have failed validation \ when `component-model-async` feature disabled" @@ -361,31 +410,20 @@ impl Func { self.post_return_impl(store, post_return_arg) } - pub(crate) fn lifted_core_func(&self, store: &mut StoreOpaque) -> NonNull { - let def = { - let instance = self.instance.id().get(store); - let (_ty, def, _options) = instance.component().export_lifted_function(self.index); - def.clone() - }; - match self.instance.lookup_vmdef(store, &def) { - Export::Function(f) => f.vm_func_ref(store), - _ => unreachable!(), - } + #[inline] + pub(crate) fn lifted_core_func(&self, store: &StoreOpaque) -> NonNull { + self.instance.id().assert_belongs_to(store.id()); + self.unsafe_func_ref.as_non_null() } + #[inline] pub(crate) fn post_return_core_func(&self, store: &StoreOpaque) -> Option> { - let instance = self.instance.id().get(store); - let component = instance.component(); - let (_ty, _def, options) = component.export_lifted_function(self.index); - let post_return = component.env_component().options[options].post_return; - post_return.map(|i| instance.runtime_post_return(i)) + self.instance.id().assert_belongs_to(store.id()); + self.post_return_func_ref.map(|p| p.as_non_null()) } - pub(crate) fn abi_async(&self, store: &StoreOpaque) -> bool { - let instance = self.instance.id().get(store); - let component = instance.component(); - let (_ty, _def, options) = component.export_lifted_function(self.index); - component.env_component().options[options].async_ + pub(crate) fn abi_async(&self) -> bool { + self.abi_async } pub(crate) fn abi_info<'a>( @@ -398,13 +436,11 @@ impl Func { &'a CanonicalOptions, ) { let vminstance = self.instance.id().get(store); - let component = vminstance.component(); - let (ty, _def, options_index) = component.export_lifted_function(self.index); - let raw_options = &component.env_component().options[options_index]; + let raw_options = &vminstance.component().env_component().options[self.options]; ( - options_index, + self.options, vminstance.instance_flags(raw_options.instance), - ty, + self.ty, raw_options, ) } @@ -446,7 +482,7 @@ impl Func { bail!(crate::Trap::CannotEnterComponent); } - let async_type = self.abi_async(store.0); + let async_type = self.abi_async(); store.0.enter_guest_sync_call(None, async_type, instance)?; #[repr(C)] @@ -550,12 +586,11 @@ impl Func { pub(crate) fn post_return_impl(&self, mut store: impl AsContextMut, arg: ValRaw) -> Result<()> { let mut store = store.as_context_mut(); - let index = self.index; let vminstance = self.instance.id().get(store.0); let component = vminstance.component(); - let (_ty, _def, options) = component.export_lifted_function(index); let post_return = self.post_return_core_func(store.0); - let flags = vminstance.instance_flags(component.env_component().options[options].instance); + let flags = + vminstance.instance_flags(component.env_component().options[self.options].instance); unsafe { call_post_return(&mut store, post_return, arg, flags)?; diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 9cac53d770cd..4e65057361e0 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -207,7 +207,7 @@ where fn call_impl(&self, mut store: impl AsContextMut, params: Params) -> Result { let mut store = store.as_context_mut(); - if self.func.abi_async(store.0) { + if self.func.abi_async() { bail!("must enable the `component-model-async` feature to call async-lifted exports") } diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 453b6dcf57e7..95468d3c9d0b 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -167,7 +167,7 @@ impl Instance { } // And package up the indices! - Some(Func::from_lifted_func(*self, index)) + Some(Func::from_lifted_func(store, *self, index)) } /// Looks up an exported [`Func`] value by name and with its type. diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index b9520fdfb522..c8ab027e51d3 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -277,7 +277,7 @@ pub struct Func { /// Note that this field has an `unsafe_*` prefix to discourage use of it. /// This is only safe to read/use if `self.store` is validated to belong to /// an ambiently provided `StoreOpaque` or similar. Use the - /// `self.func_ref()` method instead of this field to perform this check. + /// `self.vm_func_ref()` method instead of this field to perform this check. unsafe_func_ref: SendSyncPtr, }