The best WebAssembly runtime may still be no runtime at all
摘要
文章更新 2023 年观点,介绍 wasm2c(添加 wide arithmetic 支持后)与 Wasmer 7.1.0、Wasmtime 46.0.0 在 libsodium 基准上的对比。wasm2c 生成的 C 代码用 zig cc -O3 -march = native 编译(启用 Segue 内存模式),速度中位数与 Wasmer 持平,几何平均优于两者;峰值 RSS 更低,主要因无运行时引擎固定开销。指出该路径输出便携 C,支持 clang / gcc 等编译器选择(包括 Fil-C 内存安全或 CompCert 形式验证),但不适用于动态加载不可信代码、运行时策略或需 Component Model 等场景。
荐读理由
受控Wasm模块用wasm2c转C加上wide arithmetic支持后,以zig cc -O3编译,在libsodium基准上geomean耗时比Wasmtime低,RSS更低,还能换Fil-C或CompCert获得内存安全或形式化验证
原文
The best WebAssembly runtime may still be no runtime at all
In 2023, I wrote that the best WebAssembly runtime may be no runtime at all.
To summarize: if you already have a WebAssembly module, translating it to C and compiling that C with a normal native compiler can be surprisingly hard to beat.
I expected that argument to get weaker over time, because WebAssembly runtimes keep improving. Their compilers have better register allocators, better lowering, better support for newer WebAssembly instructions, and more deployment polish than a small transpiler can reasonably have.
Then I ran the 2026 benchmark.
The 2026 WebAssembly benchmark has the full table for other runtimes.
And the WebAssembly-to-C path performed very well, but was beaten by new runtimes implementing the “wide arithmetic” WebAssembly proposal.
I was curious to see how the WebAssembly-to-C approach would work with if that proposal was implemented.
So, let’s compare:
Wasmer 7.1.0Wasmtime 46.0.0WABT
wasm2c
For wasm2c, I added support for wide arithmetic, which was surprisingly trivial to implement. You can find my fork here.
The generated C code simply uses compiler carry intrinsics and C’s 128-bit integer type.
Note that I compiled the generated C with WABT’s Segue memory mode enabled, with zig cc -O3 -march=native, on the exact same libsodium benchmark suite as the previous runtime post.
Every build uses the lime1+simd128+wide_arithmetic feature set.
Oh, and before you ask: wasm2c supports memory protection using guard pages, like other runtimes.
The speed numbers
The numbers below are slowdowns relative to the native libsodium build. A value of 1.25x native means the benchmark took 25% more time than native code.
Lower is better.

The median is a tie with Wasmer. But the wasm2c executable used 0.887x as much time as Wasmer on the geomean, and 0.814x as much as Wasmtime.
The dumb C path survived the feature upgrade. Once the new arithmetic instructions were added, a normal C compiler could again produce code competitive with dedicated WebAssembly compilers.
Memory usage
I also measured peak resident set size with /usr/bin/time -v.
This is cold process RSS for one command invocation, using the median of fifteen runs. It includes the runtime executable, its startup state, JIT or compilation machinery used by that command, and anything else the process maps while running the benchmark.
Note that Wasmer and Wasmtime have a fixed cost that can be amortized if a service keeps the runtime alive and runs many modules or many calls.

Most of that difference is fixed engine overhead. The module itself costs very little.
If I subtract the RSS of an empty WASI module run through the same path, with a 64 MiB maximum linear memory like the benchmark build, only a couple of MiB remain:

At run time, with the WebAssembly code directly compiled to C, then to executable code, there’s no engine process to bring along.
No runtime is still a good choice
The generated output is portable C.
So, the deployment target no longer has to be “a platform with a WebAssembly runtime”. It can be any platform with a C compiler and enough libc support for whatever the module imports.
It also means the compiler is a choice.
You can compile the generated C with clang or gcc.
But you can also take advantage of modern C toolchains such as Fil-C to get memory safety on wasm64, or on wasm32 without depending on virtual-memory guard pages.
Or you can use a formally verified compiler such as CompCert for high assurance code generation.
I didn’t benchmark those toolchains. The point is that WebAssembly-to-C gives you that option, while a dedicated runtime doesn’t.
This is also why I remain impressed by Wasmer.
Wasmer also implements WASIX, which fills many of the POSIX-shaped gaps that plain WASI still leaves open. So, if your goal is to run existing applications in WebAssembly with fewer rewrites, this is the way to go, not WebAssembly-to-C or other options.
And Wasmtime implements the “Component Model”, that improves code separation and gives applications a typed interface boundary for composing pieces of WebAssembly. I have an intuition that the same thing can be achieved using WebAssembly-to-C by compiling components separately, but I didn’t think about it much.
Regardless WebAssembly-to-C is the wrong answer if you need dynamic loading of untrusted code without precompilation, runtime-level policy, preemption, fuel, component-model machinery, or one engine reused across many tenants.
It’s also the wrong answer if your deployment model depends on shipping one portable .wasm artifact and letting the destination machine choose how to run it.
But if you control the WebAssembly module, can precompile it, and don’t need runtime services, WebAssembly-to-C is very close to a no-brainer, especially with the addition of wide arithmetic.
这条对你有帮助吗?