wasmtime: Cache VMFuncRef in component::Func - #14189
Conversation
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" ```
| } | ||
|
|
||
| fn engines() -> Vec<(Engine, IsAsync)> { | ||
| fn engines(concurrency_support: bool) -> Vec<(Engine, IsAsync)> { |
There was a problem hiding this comment.
Note: This change to the benchmark was required to bench component calls with concurrency_support disabled. The previous setup would only run them with it enabled.
| @@ -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]; | |||
There was a problem hiding this comment.
Note: I think we could also save a lookup here by caching CanonicalOptions as well, but I didn't do it because that type has a bunch of fields and variants which I think would require either
- Making the C type messy and changing it whenever new canon opts are made. Or
- Boxing
CanonicalOptionsbefore storing it inFunc.
alexcrichton
left a comment
There was a problem hiding this comment.
Nice wins! Do you have a relative breakdown of where the wins are coming from? For example which lookup is the most expensive?
I'm a bit wary to inflate component::Func too too much to avoid duplicating information thoughout the runtime, so if some of the field movements are pretty minor in wins and one field predominantly dominates that might be a way to cut down on duplication. Naively for example I'd expect that ExportIndex would be relatively quick to lookup the type/options, and then the presence of post-return and async-ness in theory not too much more expensive when looking up the options themselves. Basically I'd expect that the unsafe_func_ref field to be the majority of the win here, but before actually changing anything here I'd want to confirm about where the performance wins come from
I didn't do a breakdown, but I can get one. Also I'm now realizing that |
|
For the case of concurrency disabled, with just the I also realized we could mostly reuse the results of I guess this means we could just use the |
alexcrichton
left a comment
There was a problem hiding this comment.
Sounds reasonable to me, and changes look good!
A core
Funccaches a raw pointer to itsVMFuncRef, but acomponent::Funcrederives the pointer on ever call and thiscontributes to host -> wasm component function calls having
significantly higher overhead than core function calls (even concurrency
support disabled).
This PR caches the
VMFuncRefforcomponent::Funcin the same it iscurrently done for core
Func. TheVMFuncReffor the associatedpost_returncall 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:
After change:
The bencmarks run are:
A separate commit also modifies the
callbenchmark to allow running component calls without concurrency support.