For those wondering, Wasm3 describes itself as "the fastest WebAssembly interpreter". Other webassembly runtimes are JIT based, rather than being interpreters. The project's readme (https://github.com/wasm3/wasm3) talks more about this decision.
I also recently landed a change in Clang trunk that offers guaranteed tail calls, so that this tail call design is safe in non-opt builds: https://reviews.llvm.org/D99517 I think wasm3 could benefit from using this attribute when it is available.
How do guaranteed tail calls work when you needs to put arguments on the stack, which usually prevents tail call? (and whether a specific call needs to pass arguments on the stack depends on the platform, and the number and type of arguments)
These rules guarantee that the tail call is possible to perform on every platform. They end up being more strict than is necessary on some platforms and calling conventions.
Some runtimes like wasmtime support AOT compilation artifacts that can be loaded.
But in general you can't "just compile" Webassembly, because you need a runtime.
There is a C project that can compile an executable that includes the runtime and the compiled WASM, but the name is escaping me right now.
Note that JIT in the WASM world doesn't quite mean the same thing as for eg Java. Almost all runtimes compile a whole module at once, not individual functions.
The core implementation idea behind wasm sandboxing is that you can compile C/C++ into wasm code, and then you can compile that wasm code into native code for the machine your program actually runs on. These steps are similar to what you’d do to run C/C++ applications in the browser, but we’re performing the wasm to native code translation ahead of time, when Firefox itself is built.
It's a middle ground between sandboxing with subprocesses (which adds all the overhead of IPC) and switching to a fully memory-safe language (Rust, JS, etc.)
If I were writing a sandboxing or instrumentation tool, operating on WASM would be much simpler than dealing with the quirks of assembly, syscalls, or some higher-level IR.
A JIT generally means it compiles parts of the program to machine code on the fly before running them, as needed.
To support that, the environment must allow the JIT to write machine code to memory, and then execute that same code.
CPUs have memory protection flags to control which memory areas can be written, and which can be executed. The OS is in charge of setting those flags, on request from the application. Eg. mmap and mprotect system calls.
iOS denies requests for memory that is both writable and executable at the same time. So applications cannot get the type of memory area a JIT needs. There are indirect methods where a file is written then mapped, like generating a small program or shared library on the fly. But iOS restricts these as well.
There are workarounds for a developer's personal applications, used on their own registered iOS devices. But these workarounds cannot be run by everyone else, except people with a jailbroken iOS. They cannot be used in applications on the App Store.
W^X is not the issue here. JITs can deal with that, and in fact should do so even when it's not OS-enforced for security reasons. The problem on ios specifically is code signing, which is a problem for both JIT and AOT.
"There are indirect methods where a file is written then mapped, like generating a small program or shared library on the fly. But iOS restricts these as well."
Note that it is possible to JIT even with W^X. What these platforms do is prevent memory that has been mapped writable and overwritten with new code from ever being mapped executable again.
So mostly I'm interested in compilers that compile the entire thing before any execution happens. Technically both AoT and JIT compilers can do that, but 95% of the things associated with "JIT" involve compiling the program one piece at a time.
But even with the broad definition of JIT, one important difference is that AoT compilers don't have to mark pages executable.
I think the model is typically that optimisation applied in "normal compilation" are applied when the source language is translated to WASM, meaning that the WASM -> native translation can be quite straightforward and still performant.
Bounds checking, dynamic dispatch is more expensive, wasm semantics have no undefined behavior to exploit for optimizations. And there is bloat in the process. I’m sure losing information effects things as well
Comments
For those wondering, Wasm3 describes itself as "the fastest WebAssembly interpreter". Other webassembly runtimes are JIT based, rather than being interpreters. The project's readme (https://github.com/wasm3/wasm3) talks more about this decision.
For more on the difference, and an explanation of what JIT is, check out this section of the book Crafting Interpreters (https://craftinginterpreters.com/a-map-of-the-territory.html...)
Wasm3 calls its interpreter design a "meta machine": https://github.com/wasm3/wasm3/blob/main/docs/Interpreter.md...
It is heavily based around tail calls. I recently wrote a blog article about how we applied a similar tail-call-oriented strategy to accelerate protobuf parsing to 2+GB/s: https://blog.reverberate.org/2021/04/21/musttail-efficient-i...
I also recently landed a change in Clang trunk that offers guaranteed tail calls, so that this tail call design is safe in non-opt builds: https://reviews.llvm.org/D99517 I think wasm3 could benefit from using this attribute when it is available.
How do guaranteed tail calls work when you needs to put arguments on the stack, which usually prevents tail call? (and whether a specific call needs to pass arguments on the stack depends on the platform, and the number and type of arguments)
The musttail attribute performs several checks to ensure that a tail call can be guaranteed, and rejects the program if these rules are violated. The documentation has the details: https://github.com/llvm/llvm-project/blob/3b8ec86fd576b9808d...
These rules guarantee that the tail call is possible to perform on every platform. They end up being more strict than is necessary on some platforms and calling conventions.
Do none of the notable ones do normal compilation?
Some runtimes like wasmtime support AOT compilation artifacts that can be loaded.
But in general you can't "just compile" Webassembly, because you need a runtime.
There is a C project that can compile an executable that includes the runtime and the compiled WASM, but the name is escaping me right now.
Note that JIT in the WASM world doesn't quite mean the same thing as for eg Java. Almost all runtimes compile a whole module at once, not individual functions.
Compiling individual functions was something in Java JITs like 20 years ago, they are a little bit more clever nowadays.
What would the difference be between “JIT” and “normal” compilation for WebAssembly?
An AOT compiler would do a one-time translation to a platform-specific binary.
If the environment you run on doesn’t support JIT compilation (iOS for example), AOT compiling WASM is useful.
At that point, what's the purpose of using WASM at all?
You can keep the sandbox intact after AOT.
https://hacks.mozilla.org/2020/02/securing-firefox-with-weba...
It's a middle ground between sandboxing with subprocesses (which adds all the overhead of IPC) and switching to a fully memory-safe language (Rust, JS, etc.)
Control Flow Integrity, https://en.wikipedia.org/wiki/Control-flow_integrity
If I were writing a sandboxing or instrumentation tool, operating on WASM would be much simpler than dealing with the quirks of assembly, syscalls, or some higher-level IR.
How can a platform not support j.i.t. compilation?
In what way does a platform need to coöperate with that?
A JIT generally means it compiles parts of the program to machine code on the fly before running them, as needed.
To support that, the environment must allow the JIT to write machine code to memory, and then execute that same code.
CPUs have memory protection flags to control which memory areas can be written, and which can be executed. The OS is in charge of setting those flags, on request from the application. Eg. mmap and mprotect system calls.
iOS denies requests for memory that is both writable and executable at the same time. So applications cannot get the type of memory area a JIT needs. There are indirect methods where a file is written then mapped, like generating a small program or shared library on the fly. But iOS restricts these as well.
There are workarounds for a developer's personal applications, used on their own registered iOS devices. But these workarounds cannot be run by everyone else, except people with a jailbroken iOS. They cannot be used in applications on the App Store.
W^X is not the issue here. JITs can deal with that, and in fact should do so even when it's not OS-enforced for security reasons. The problem on ios specifically is code signing, which is a problem for both JIT and AOT.
"There are indirect methods where a file is written then mapped, like generating a small program or shared library on the fly. But iOS restricts these as well."
Some platforms are enforcing https://en.wikipedia.org/wiki/W%5EX because malware also wants to create novel machine code within a running process.
Note that it is possible to JIT even with W^X. What these platforms do is prevent memory that has been mapped writable and overwritten with new code from ever being mapped executable again.
So mostly I'm interested in compilers that compile the entire thing before any execution happens. Technically both AoT and JIT compilers can do that, but 95% of the things associated with "JIT" involve compiling the program one piece at a time.
But even with the broad definition of JIT, one important difference is that AoT compilers don't have to mark pages executable.
I think the model is typically that optimisation applied in "normal compilation" are applied when the source language is translated to WASM, meaning that the WASM -> native translation can be quite straightforward and still performant.
I worked on one as an undergrad: https://github.com/gwsystems/aWsm
Full AoT compilation, C programs run within 10% of native
10% difference from native, or 10% of native performance?
10% worse performance. Not 10x worse :D
Since it compiles using llvm, what's the reason for the discrepancy?
Lost information about e.g. aliasing?
Bounds checking, dynamic dispatch is more expensive, wasm semantics have no undefined behavior to exploit for optimizations. And there is bloat in the process. I’m sure losing information effects things as well
wasm2c/WasmBoxC springs to mind:
https://kripken.github.io/blog/wasm/2020/07/27/wasmboxc.html
WAVM and SSVM do normal compilation.