JIT Compilation
Relevant source files
The following files were used as context for generating this wiki page:
JIT (Just-In-Time) compilation in AtomVM is an experimental feature that translates Erlang/BEAM bytecode into native machine code at runtime. It is designed to improve performance on supported architectures by executing native instructions instead of interpreting bytecode via the standard emulator loop. AtomVM supports four execution modes: Emulated (default), JIT (runtime compilation), Native (ahead-of-time compilation), and Hybrid (mixing native and emulated code).
Sources: doc/src/atomvm-internals.md:214-232
Execution Modes and Architecture
AtomVM provides flexibility in how code is executed, catering to different hardware constraints and performance requirements.
Comparison of Execution Modes
| Mode | Description | Components Included |
|---|---|---|
| Emulated | Standard bytecode interpretation. | VM Interpreter |
| JIT | Bytecode is compiled to native code at runtime. | JIT Compiler (Erlang) + Native Backend |
| Native | Code is precompiled on a desktop (AOT) and run on device. | Native execution engine only |
| Hybrid | Supports both native and emulated BEAM code. | VM Interpreter + Native execution engine |
Sources: doc/src/atomvm-internals.md:214-226
Target Architectures
The JIT system identifies its target architecture at compile time via CMAKE_SYSTEM_PROCESSOR or the AVM_JIT_TARGET_ARCH definition. Supported backends include:
- x86_64: System V ABI
- aarch64: ARM 64-bit ABI
- armv6m: ARM 32-bit ABI (used in Raspberry Pi Pico/STM32)
- riscv32: rv32imc ilp32 ABI
Sources: doc/src/atomvm-internals.md:231-255, src/libAtomVM/jit.c:51-68
Compiler Internal Workflow
The JIT compiler is primarily written in Erlang and interacts with the VM via the code_server. When a process attempts to call a function in a module that lacks native code, the process is trapped, and a message is sent to the code_server to initiate compilation.
The Compilation Loop
The diagram shows the transition from a trapped bytecode call to native execution after JIT compilation.
Sources: doc/src/atomvm-internals.md:244-249
JIT Interfaces
The compiler is divided into two primary interfaces:
- Backend: Architecture-specific implementations (e.g.,
jit_x86_64,jit_riscv32) that translate bytecode to machine instructions. - Stream: Handles the output of machine code. Different streams are used depending on the platform:
jit_stream_binary: Streams to an Erlang binary (useful for AOT/Testing).jit_stream_mmap: Allocates memory pages on Unix systems.jit_stream_flash: Writes directly to flash memory on microcontrollers like the Pico.
Sources: doc/src/atomvm-internals.md:251-262, libs/jit/src/CMakeLists.txt:28-41
Native Interface and Runtime Integration
The VM interacts with compiled code through the ModuleNativeInterface. This structure provides the native code with hooks into the VM's core functions, such as memory allocation, term comparison, and process scheduling.
Key Native Interface Functions
| Function | Description |
|---|---|
jit_call_ext | Handles calls to external modules or NIFs. |
jit_test_heap | Checks and ensures sufficient heap space for operations. |
jit_alloc_boxed_integer_fragment | Allocates memory for large integers on the heap. |
jit_process_signal_messages | Processes incoming signals (Kill, GC, etc.) during execution. |
jit_handle_error | Manages exceptions and stacktrace generation for native code. |
Sources: src/libAtomVM/jit.c:1040-1120
Signal and Message Handling
Even while executing native code, AtomVM processes must remain responsive to signals. The jit_process_signal_messages function handles various signal types:
- KillSignal: Terminates the process.
- GCSignal: Forces a garbage collection.
- CodeServerResumeSignal: Resumes a process after the code server has finished JIT compilation.
Sources: src/libAtomVM/jit.c:646-750
Memory Management in JIT
Memory management is critical for JIT, especially on embedded devices. The jit_state structure maintains the runtime context for the JIT engine, including the current module and remaining reductions.
The relationship between the process Context and the JITState used for native execution.
Sources: src/libAtomVM/jit.c:51-85, src/libAtomVM/jit.h:45-60
Embedded JIT (Flash Streaming)
On devices like the Raspberry Pi Pico, JIT compilation can write directly to flash. This allows the compiled native code to persist across reboots. AtomVM uses a specific partitioning scheme to accommodate the JIT library and the flash-stored native code. If the application or library is updated, the JIT-ed segments are invalidated and recompiled on next use.
Sources: doc/src/atomvm-internals.md:264-285
Summary
The AtomVM JIT system provides a high-performance alternative to bytecode interpretation by leveraging architecture-specific backends and flexible streaming mechanisms. By integrating runtime compilation with the code_server and providing a robust ModuleNativeInterface, AtomVM allows Erlang and Elixir applications to run with native efficiency even on resource-constrained microcontrollers. While experimental, it supports persistent native code via flash streaming on embedded platforms and standard memory-mapped execution on Unix-like systems.
Sources: doc/src/atomvm-internals.md:214-232, src/libAtomVM/jit.c:1040-1120