Repository navigation
Instantiation of modules with large element segments into imported tables regressed ~7x since #13487 #14616
Description
Instantiating a core module whose active element segment writes many functions into an imported table is about 7x slower on Wasmtime 46+ than on 36. The same segment into a table defined by the module is unaffected.
Numbers
5,000-element segment, Engine::default(), x86_64 Linux (Intel Core Ultra 7 255H), mean of 2,000 Instance::new calls after 200 warm-up calls, load average < 2:
| version | imported table | table defined in module |
|---|---|---|
| 36.0.17 | 27.6 us | 0.53 us |
| 48.0.5 | 180.2 us | 0.73 us |
| 49.0.2 | 180.0 us | 0.73 us |
| 50.0.0-rc.1 | 195.4 us | 0.75 us |
That is roughly 5.5 ns per element on 36 against roughly 36 ns per element on 48+.
Reproducer
Cargo.toml:
[package]
name = "repro"
version = "0.1.0"
edition = "2024"
[dependencies]
w36 = { package = "wasmtime", version = "=36.0.17" }
w48 = { package = "wasmtime", version = "=48.0.5" }
w49 = { package = "wasmtime", version = "=49.0.2" }
w50 = { package = "wasmtime", version = "=50.0.0-rc.1" }src/main.rs (run with cargo run --release):
// N functions written by an active element segment into an imported table.
fn wat(n: usize, imported: bool) -> String {
let funcs: String = (0..n).map(|i| format!("(func (result i32) i32.const {i})\n")).collect();
let idx: Vec<String> = (0..n).map(|i| i.to_string()).collect();
let table = if imported {
format!("(import \"env\" \"t\" (table {n} funcref))")
} else {
format!("(table {n} funcref)")
};
format!("(module {table}\n{funcs}(elem (i32.const 0) func {}))", idx.join(" "))
}
macro_rules! bench {
($w:ident) => {{
use $w::*;
let engine = Engine::default();
for (n, imported) in [(5000, true), (5000, false)] {
let module = Module::new(&engine, wat(n, imported)).unwrap();
let (iters, mut total) = (2000, std::time::Duration::ZERO);
for k in 0..iters + 200 {
let mut store = Store::new(&engine, ());
let ty = TableType::new(RefType::FUNCREF, n as u32, None);
let t = Table::new(&mut store, ty, Ref::Func(None)).unwrap();
let imports: Vec<Extern> = if imported { vec![t.into()] } else { vec![] };
let t0 = std::time::Instant::now();
Instance::new(&mut store, &module, &imports).unwrap();
if k >= 200 { total += t0.elapsed(); }
}
println!("{:>12} n={n} imported={imported:<5}: {:8.2} us",
stringify!($w), total.as_secs_f64() * 1e6 / iters as f64);
}
}};
}
fn main() {
bench!(w36);
bench!(w48);
bench!(w49);
bench!(w50);
}Suspected mechanism
#13487 ("Move most module initialization to compiled code", first released in 46.0.0) moved element segment initialisation into the compiled module startup function. An active segment into an imported table cannot be precomputed, so it appears to compile to one ref_func libcall plus a table store per element, where 36 applied the whole segment host-side in a single pass. The per-call cost of the ref_func libcall (which looks up the store's code for the function index each time) then dominates. #13295 looks like the same libcall cost seen from a hot loop.
Config does not seem to change it: on 48, disabling GC, function references and exceptions, or toggling table_lazy_init, left the Python component case unchanged in our testing.
Real-world impact
componentize-py components link CPython as wasm shared libraries, and those core modules write about 6,100 functions into the imported env.__indirect_function_table at (global.get $__table_base). On a platform running componentize-py components, instantiating a Python component went from about 90 us on 36 to about 375 us on 48, and the element segments account for most of the difference (about 6,100 x 40 ns).
Possible directions, if useful: apply segments into imported tables host-side in one libcall per segment (as 36 did), or a cheaper path for ref.func of a module-defined function inside the startup function.
Activity
alexcrichton commentedon Oct 12, 2026
Thanks for the report! The exact cause of this isn't quite the PR you linked (#13487) but instead a series of other changes and refactorings to how things happen internally. I've drafted a change at #14683 which should significantly speed things up here, even moreso than the baseline you're using here.
Optimize the implementation of the `ref.func` instruction
Optimize the implementation of the `ref.func` instruction
Optimize the implementation of `ref.func` (bytecodealliance#14683)
alexcrichton commentedon Oct 27, 2026
I believe this is fixed in #14683, so closing.