Skip to content

wasmtime: Cache VMFuncRef in component::Func - #14189

Merged
alexcrichton merged 3 commits into
bytecodealliance:mainfrom
adamrk:cache-component-funcref
Aug 24, 2026
Merged

wasmtime: Cache VMFuncRef in component::Func#14189
alexcrichton merged 3 commits into
bytecodealliance:mainfrom
adamrk:cache-component-funcref

Conversation

@adamrk

@adamrk adamrk commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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"

A separate commit also modifies the call benchmark to allow running component calls without concurrency support.

adamrk added 2 commits August 21, 2026 18:50
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"
```
@adamrk
adamrk requested review from a team as code owners August 21, 2026 19:00
@adamrk
adamrk requested review from alexcrichton and pchickey and removed request for a team August 21, 2026 19:00
@github-actions github-actions Bot added wasmtime:api Related to the API of the `wasmtime` crate itself wasmtime:c-api Issues pertaining to the C API. labels Aug 21, 2026
Comment thread benches/call.rs
}

fn engines() -> Vec<(Engine, IsAsync)> {
fn engines(concurrency_support: bool) -> Vec<(Engine, IsAsync)> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +429 to +439
@@ -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];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Making the C type messy and changing it whenever new canon opts are made. Or
  2. Boxing CanonicalOptions before storing it in Func.

@alexcrichton
alexcrichton removed the request for review from pchickey August 24, 2026 15:06

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@adamrk

adamrk commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Do you have a relative breakdown of where the wins are coming from? For example which lookup is the most expensive?

I didn't do a breakdown, but I can get one. Also I'm now realizing that abi_info gets called a few times, so maybe even if we don't include other fields in Func we can at least get some improvement by just calling abi_info once and reusing the result.

@adamrk

adamrk commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

For the case of concurrency disabled, with just the unsafe_func_ref field I'm seeing a time of 180ns (from the 300ns original). So all the other fields together are an additional ~40ns. It's a bit hard to divide up the remaining amount between specific other fields because I'm seeing 10-20ns swings between runs anyway.

I also realized we could mostly reuse the results of abi_info in the non-concurrent case without caching them in Func and that seems to give us most of the benefits anyway. With the existing changes I'm now seeing 140ns with concurrency disabled (same as the original change) and 730ns with concurrency enabled (not as big as the original change).

I guess this means we could just use the unsafe_func_ref field and there's probably opportunity to improve the concurrent case as a follow up by reusing the results of abi_info more.

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable to me, and changes look good!

@alexcrichton
alexcrichton added this pull request to the merge queue Aug 24, 2026
Merged via the queue into bytecodealliance:main with commit ffb0408 Aug 24, 2026
58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wasmtime:api Related to the API of the `wasmtime` crate itself wasmtime:c-api Issues pertaining to the C API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants