JIT vs AOT¶
anvil supports two execution modes from the same NumericalFunction: JIT (Just-In-Time) for Python calling and AOT (Ahead-Of-Time) for standalone C++ deployment. Both modes use the same codegen pipeline and produce equivalent native code.
Overview¶
flowchart TB
NF["NumericalFunction"] --> AOT["AOT: generate_module()"]
NF --> JIT["JIT: fn(x)"]
AOT --> HPP[".hpp + .cpp<br/>Standalone files"]
HPP --> Build["Your build system<br/>(CMake, Make, ...)"]
Build --> Binary["Embedded binary"]
JIT --> Src["Single .cpp<br/>(self-contained)"]
Src --> Clang["clang++ -shared -O2"]
Clang --> Dylib[".dylib/.so"]
Dylib --> Ctypes["ctypes.CDLL"]
Ctypes --> Call["fn(np.ndarray) → np.ndarray"]
AOT: generate_module()¶
Produces standalone .hpp and .cpp files that can be compiled into any C++ project:
- Each function gets its own namespace inside the module namespace
- Multiple functions share the same
Buffer<T, Ns...>template - SQP solvers include the full optimization loop, linking against PIQP
- No Python dependency at runtime
JIT: fn(x)¶
On the first call, the function is compiled to a shared library and loaded via ctypes. Subsequent calls go straight to native code:
sequenceDiagram
participant User
participant NF as NumericalFunction
participant Cache as ~/.cache/anvil/jit/
User->>NF: fn(x) (first call)
NF->>NF: generate C++ source
NF->>NF: SHA256(source)
NF->>Cache: check for cached .dylib
alt Cache miss
NF->>NF: clang++ -shared -O2 → .tmp
NF->>Cache: atomic rename .tmp → .dylib
end
NF->>NF: ctypes.CDLL(dylib)
NF->>NF: init_ws()
NF->>User: result (np.ndarray)
User->>NF: fn(x) (subsequent calls)
NF->>NF: call native function
NF->>User: result (np.ndarray)
Caching¶
Compiled shared libraries are cached in ~/.cache/anvil/jit/ keyed by {name}_{sha256(source)}.{dylib|so}. Compilation writes to a .tmp file first, then atomically renames, so concurrent processes don't corrupt the cache.
Workspace lifecycle¶
- NumericalFunction:
init_ws()allocates a flatBuffer<char, N>for intermediate buffers. Cleanup viaweakref.finalize. - SQP:
init_ws()allocates the fullWsstruct (PIQP workspace, QP data, all intermediate buffers).deinit_ws()calls PIQP cleanup and frees all buffers. Also cleaned up viaweakref.finalize.
What differs between JIT and AOT¶
| Aspect | JIT | AOT |
|---|---|---|
| Output | Single self-contained .cpp |
Separate .hpp + .cpp |
| C interface | extern "C" wrappers for ctypes |
C++ namespaced functions |
| Buffer passing | Raw void* pointers |
Typed Buffer<T, Ns...> references |
| Constants | Same: static constexpr arrays |
Same |
| Kernels | Same rendered source | Same |
| Compilation | Automatic (clang++) | User's build system |
| SQP linking | -Wl,-rpath for PIQP discovery |
User links PIQP manually |
The JIT templates (jit_module.j2, jit_shim_*.j2) add extern "C" wrappers around the same C++ code that the AOT templates produce. This is necessary because ctypes can only call C functions, not C++ namespaced functions.