Skip to content

Commit 5f37933

Browse files
committed
Optimize Func::call and its C API
This commit is an alternative to #3298 which achieves effectively the same goal of optimizing the `Func::call` API as well as its C API sibling of `wasmtime_func_call`. The strategy taken here is different than #3298 though where a new API isn't created, rather a small tweak to an existing API is done. Specifically this commit handles the major sources of slowness with `Func::call` with: * Looking up the type of a function, to typecheck the arguments with and use to guide how the results should be loaded, no longer hits the rwlock in the `Engine` but instead each `Func` contains its own `FuncType`. This can be an unnecessary allocation for funcs not used with `Func::call`, so this is a downside of this implementation relative to #3298. A mitigating factor, though, is that instance exports are loaded lazily into the `Store` and in theory not too many funcs are active in the store as `Func` objects. * Temporary storage is amortized with a long-lived `Vec` in the `Store` rather than allocating a new vector on each call. This is basically the same strategy as #3294 only applied to different types in different places. Specifically `wasmtime::Store` now retains a `Vec<u128>` for `Func::call`, and the C API retains a `Vec<Val>` for calling `Func::call`. * Finally, an API breaking change is made to `Func::call` and its type signature (as well as `Func::call_async`). Instead of returning `Box<[Val]>` as it did before this function now takes a `results: &mut [Val]` parameter. This allows the caller to manage the allocation and we can amortize-remove it in `wasmtime_func_call` by using space after the parameters in the `Vec<Val>` we're passing in. This change is naturally a breaking change and we'll want to consider it carefully, but mitigating factors are that most embeddings are likely using `TypedFunc::call` instead and this signature taking a mutable slice better aligns with `Func::new` which receives a mutable slice for the results. Overall this change, in the benchmark of "call a nop function from the C API" is not quite as good as #3298. It's still a bit slower, on the order of 15ns, because there's lots of capacity checks around vectors and the type checks are slightly less optimized than before. Overall though this is still significantly better than today because allocations and the rwlock to acquire the type information are both avoided. I personally feel that this change is the best to do because it has less of an API impact than #3298.
1 parent 8ebaaf9 commit 5f37933

File tree

26 files changed

+398
-253
lines changed

26 files changed

+398
-253
lines changed

crates/c-api/src/func.rs

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use crate::{
33
wasm_extern_t, wasm_functype_t, wasm_store_t, wasm_val_t, wasm_val_vec_t, wasmtime_error_t,
44
wasmtime_extern_t, wasmtime_val_t, wasmtime_val_union, CStoreContext, CStoreContextMut,
55
};
6-
use anyhow::anyhow;
76
use std::ffi::c_void;
87
use std::mem::{self, MaybeUninit};
98
use std::panic::{self, AssertUnwindSafe};
@@ -109,6 +108,22 @@ pub unsafe extern "C" fn wasm_func_new_with_env(
109108
})
110109
}
111110

111+
/// Places the `args` into `dst` and additionally reserves space in `dst` for `results_size`
112+
/// returns. The params/results slices are then returned separately.
113+
fn translate_args<'a>(
114+
dst: &'a mut Vec<Val>,
115+
args: impl ExactSizeIterator<Item = Val>,
116+
results_size: usize,
117+
) -> (&'a [Val], &'a mut [Val]) {
118+
debug_assert!(dst.is_empty());
119+
let num_args = args.len();
120+
dst.reserve(args.len() + results_size);
121+
dst.extend(args);
122+
dst.extend((0..results_size).map(|_| Val::null()));
123+
let (a, b) = dst.split_at_mut(num_args);
124+
(a, b)
125+
}
126+
112127
#[no_mangle]
113128
pub unsafe extern "C" fn wasm_func_call(
114129
func: &mut wasm_func_t,
@@ -118,23 +133,20 @@ pub unsafe extern "C" fn wasm_func_call(
118133
let f = func.func();
119134
let results = (*results).as_uninit_slice();
120135
let args = (*args).as_slice();
121-
if results.len() != f.ty(func.ext.store.context()).results().len() {
122-
return Box::into_raw(Box::new(wasm_trap_t::new(
123-
anyhow!("wrong number of results provided").into(),
124-
)));
125-
}
126-
let params = args.iter().map(|i| i.val()).collect::<Vec<_>>();
136+
let mut dst = Vec::new();
137+
let (wt_params, wt_results) =
138+
translate_args(&mut dst, args.iter().map(|i| i.val()), results.len());
127139

128140
// We're calling arbitrary code here most of the time, and we in general
129141
// want to try to insulate callers against bugs in wasmtime/wasi/etc if we
130142
// can. As a result we catch panics here and transform them to traps to
131143
// allow the caller to have any insulation possible against Rust panics.
132144
let result = panic::catch_unwind(AssertUnwindSafe(|| {
133-
f.call(func.ext.store.context_mut(), &params)
145+
f.call(func.ext.store.context_mut(), wt_params, wt_results)
134146
}));
135147
match result {
136-
Ok(Ok(out)) => {
137-
for (slot, val) in results.iter_mut().zip(out.into_vec().into_iter()) {
148+
Ok(Ok(())) => {
149+
for (slot, val) in results.iter_mut().zip(wt_results.iter().cloned()) {
138150
crate::initialize(slot, wasm_val_t::from_val(val));
139151
}
140152
ptr::null_mut()
@@ -261,35 +273,39 @@ pub(crate) unsafe fn c_callback_to_rust_fn(
261273

262274
#[no_mangle]
263275
pub unsafe extern "C" fn wasmtime_func_call(
264-
store: CStoreContextMut<'_>,
276+
mut store: CStoreContextMut<'_>,
265277
func: &Func,
266278
args: *const wasmtime_val_t,
267279
nargs: usize,
268280
results: *mut MaybeUninit<wasmtime_val_t>,
269281
nresults: usize,
270282
trap_ret: &mut *mut wasm_trap_t,
271283
) -> Option<Box<wasmtime_error_t>> {
272-
if nresults != func.ty(&store).results().len() {
273-
return Some(Box::new(wasmtime_error_t::from(anyhow!(
274-
"wrong number of results provided"
275-
))));
276-
}
277-
let params = crate::slice_from_raw_parts(args, nargs)
278-
.iter()
279-
.map(|i| i.to_val())
280-
.collect::<Vec<_>>();
284+
let mut store = store.as_context_mut();
285+
let mut params = mem::take(&mut store.data_mut().wasm_val_storage);
286+
let (wt_params, wt_results) = translate_args(
287+
&mut params,
288+
crate::slice_from_raw_parts(args, nargs)
289+
.iter()
290+
.map(|i| i.to_val()),
291+
nresults,
292+
);
281293

282294
// We're calling arbitrary code here most of the time, and we in general
283295
// want to try to insulate callers against bugs in wasmtime/wasi/etc if we
284296
// can. As a result we catch panics here and transform them to traps to
285297
// allow the caller to have any insulation possible against Rust panics.
286-
let result = panic::catch_unwind(AssertUnwindSafe(|| func.call(store, &params)));
298+
let result = panic::catch_unwind(AssertUnwindSafe(|| {
299+
func.call(&mut store, wt_params, wt_results)
300+
}));
287301
match result {
288-
Ok(Ok(out)) => {
302+
Ok(Ok(())) => {
289303
let results = crate::slice_from_raw_parts_mut(results, nresults);
290-
for (slot, val) in results.iter_mut().zip(out.into_vec().into_iter()) {
291-
crate::initialize(slot, wasmtime_val_t::from_val(val));
304+
for (slot, val) in results.iter_mut().zip(wt_results.iter()) {
305+
crate::initialize(slot, wasmtime_val_t::from_val(val.clone()));
292306
}
307+
params.truncate(0);
308+
store.data_mut().wasm_val_storage = params;
293309
None
294310
}
295311
Ok(Err(trap)) => match trap.downcast::<Trap>() {

crates/c-api/src/store.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use crate::{wasm_engine_t, wasmtime_error_t, wasmtime_val_t, ForeignData};
22
use std::cell::UnsafeCell;
33
use std::ffi::c_void;
44
use std::sync::Arc;
5-
use wasmtime::{AsContext, AsContextMut, InterruptHandle, Store, StoreContext, StoreContextMut};
5+
use wasmtime::{
6+
AsContext, AsContextMut, InterruptHandle, Store, StoreContext, StoreContextMut, Val,
7+
};
68

79
/// This representation of a `Store` is used to implement the `wasm.h` API.
810
///
@@ -71,6 +73,10 @@ pub struct StoreData {
7173
/// Temporary storage for usage during a wasm->host call to store values
7274
/// in a slice we pass to the C API.
7375
pub hostcall_val_storage: Vec<wasmtime_val_t>,
76+
77+
/// Temporary storage for usage during host->wasm calls, same as above but
78+
/// for a different direction.
79+
pub wasm_val_storage: Vec<Val>,
7480
}
7581

7682
#[no_mangle]
@@ -90,6 +96,7 @@ pub extern "C" fn wasmtime_store_new(
9096
#[cfg(feature = "wasi")]
9197
wasi: None,
9298
hostcall_val_storage: Vec::new(),
99+
wasm_val_storage: Vec::new(),
93100
},
94101
),
95102
})

crates/fuzzing/src/oracles.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,10 @@ pub fn differential_execution(
297297

298298
let ty = f.ty(&store);
299299
let params = dummy::dummy_values(ty.params());
300+
let mut results = vec![Val::I32(0); ty.results().len()];
300301
let this_result = f
301-
.call(&mut store, &params)
302+
.call(&mut store, &params, &mut results)
303+
.map(|()| results.into())
302304
.map_err(|e| e.downcast::<Trap>().unwrap());
303305

304306
let existing_result = export_func_results
@@ -312,7 +314,7 @@ pub fn differential_execution(
312314
match instance.get_export(&mut *store, "hangLimitInitializer") {
313315
None => return,
314316
Some(Extern::Func(f)) => {
315-
f.call(store, &[])
317+
f.call(store, &[], &mut [])
316318
.expect("initializing the hang limit should not fail");
317319
}
318320
Some(_) => panic!("unexpected hangLimitInitializer export"),
@@ -478,7 +480,8 @@ pub fn make_api_calls(api: crate::generators::api::ApiCalls) {
478480
let f = &funcs[nth];
479481
let ty = f.ty(&store);
480482
let params = dummy::dummy_values(ty.params());
481-
let _ = f.call(store, &params);
483+
let mut results = vec![Val::I32(0); ty.results().len()];
484+
let _ = f.call(store, &params, &mut results);
482485
}
483486
}
484487
}
@@ -549,7 +552,7 @@ pub fn table_ops(
549552
let args: Vec<_> = (0..ops.num_params())
550553
.map(|_| Val::ExternRef(Some(ExternRef::new(CountDrops(num_dropped.clone())))))
551554
.collect();
552-
let _ = run.call(&mut store, &args);
555+
let _ = run.call(&mut store, &args, &mut []);
553556
}
554557

555558
assert_eq!(num_dropped.load(SeqCst), ops.num_params() as usize);
@@ -653,7 +656,7 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con
653656

654657
// Introspect wasmtime module to find name of an exported function and of an
655658
// exported memory.
656-
let (func_name, _ty) = first_exported_function(&wasmtime_module)?;
659+
let (func_name, ty) = first_exported_function(&wasmtime_module)?;
657660
let memory_name = first_exported_memory(&wasmtime_module)?;
658661

659662
let wasmi_mem_export = wasmi_instance.export_by_name(memory_name).unwrap();
@@ -668,8 +671,10 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con
668671
let wasmtime_main = wasmtime_instance
669672
.get_func(&mut wasmtime_store, func_name)
670673
.expect("function export is present");
671-
let wasmtime_vals = wasmtime_main.call(&mut wasmtime_store, &[]);
672-
let wasmtime_val = wasmtime_vals.map(|v| v.iter().next().cloned());
674+
let mut wasmtime_results = vec![Val::I32(0); ty.results().len()];
675+
let wasmtime_val = wasmtime_main
676+
.call(&mut wasmtime_store, &[], &mut wasmtime_results)
677+
.map(|()| wasmtime_results.get(0).cloned());
673678

674679
debug!(
675680
"Successful execution: wasmi returned {:?}, wasmtime returned {:?}",
@@ -831,15 +836,17 @@ fn run_in_wasmtime(
831836
.context("Wasmtime cannot instantiate module")?;
832837

833838
// Find the first exported function.
834-
let (func_name, _ty) =
839+
let (func_name, ty) =
835840
first_exported_function(&wasmtime_module).context("Cannot find exported function")?;
836841
let wasmtime_main = wasmtime_instance
837842
.get_func(&mut wasmtime_store, &func_name[..])
838843
.expect("function export is present");
839844

840845
// Execute the function and return the values.
841-
let wasmtime_vals = wasmtime_main.call(&mut wasmtime_store, params);
842-
wasmtime_vals.map(|v| v.to_vec())
846+
let mut results = vec![Val::I32(0); ty.results().len()];
847+
wasmtime_main
848+
.call(&mut wasmtime_store, params, &mut results)
849+
.map(|()| results)
843850
}
844851

845852
// Introspect wasmtime module to find the name of the first exported function.

crates/fuzzing/src/oracles/v8.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ pub fn differential_v8_execution(wasm: &[u8], config: &crate::generators::Config
9393
let wasmtime_main = wasmtime_instance
9494
.get_func(&mut wasmtime_store, func)
9595
.expect("function export is present");
96-
let wasmtime_vals = wasmtime_main.call(&mut wasmtime_store, &wasmtime_params);
96+
let mut wasmtime_vals = vec![Val::I32(0); ty.results().len()];
97+
let wasmtime_result =
98+
wasmtime_main.call(&mut wasmtime_store, &wasmtime_params, &mut wasmtime_vals);
9799
log::trace!("finished wasmtime invocation");
98100

99101
// V8: call the first exported func
@@ -112,23 +114,23 @@ pub fn differential_v8_execution(wasm: &[u8], config: &crate::generators::Config
112114
log::trace!("finished v8 invocation");
113115

114116
// Verify V8 and wasmtime match
115-
match (wasmtime_vals, v8_vals) {
116-
(Ok(wasmtime), Ok(v8)) => {
117+
match (wasmtime_result, v8_vals) {
118+
(Ok(()), Ok(v8)) => {
117119
log::trace!("both executed successfully");
118-
match wasmtime.len() {
120+
match wasmtime_vals.len() {
119121
0 => assert!(v8.is_undefined()),
120-
1 => assert_val_match(&wasmtime[0], &v8, &mut scope),
122+
1 => assert_val_match(&wasmtime_vals[0], &v8, &mut scope),
121123
_ => {
122124
let array = v8::Local::<'_, v8::Array>::try_from(v8).unwrap();
123-
for (i, wasmtime) in wasmtime.iter().enumerate() {
125+
for (i, wasmtime) in wasmtime_vals.iter().enumerate() {
124126
let v8 = array.get_index(&mut scope, i as u32).unwrap();
125127
assert_val_match(wasmtime, &v8, &mut scope);
126128
// ..
127129
}
128130
}
129131
}
130132
}
131-
(Ok(_), Err(msg)) => {
133+
(Ok(()), Err(msg)) => {
132134
panic!("wasmtime succeeded at invocation, v8 failed: {}", msg)
133135
}
134136
(Err(err), Ok(_)) => {

0 commit comments

Comments
 (0)