Deployment modes¶
One Fortran source, four delivery forms. The bridge calls do not change between them; what changes is how the Julia code behind those calls reaches the machine – interpreted from a project environment, precompiled into a sysimage the embedded runtime loads, or compiled ahead of time into a plain shared library – and, at the end of the line, not using Julia at all. That fourth form, native pure Fortran, is the baseline the other three are measured against.
The spectrum¶
Measured on the capstone reacting-flow example (full report:
examples/07_capstone/PERFORMANCE.md):
| Mode | Startup | What the target machine needs | Julia on the target? |
|---|---|---|---|
| JIT (embedded, project env) | 17.3 s init | your binary, a pinned Julia install, the project env and its package depot | yes |
| Sysimage | 1.90 s init | your binary plus the sysimage file, built by the same Julia | yes |
| AOT library | 0.58–1.97 s library init | your binary plus the compiled .so |
no |
| Native (pure Fortran) | none | your binary | no |
As a ladder: 17.3 s from cold start to first result in JIT, 1.90 s with a sysimage, and the AOT build answers its first call in about 12 μs, approaching the native baseline of having no Julia in the process at all. (The 12 μs is a trivial call into the compiled library; a first real chemistry call costs milliseconds of solver work in every mode, because the work sets that number, not the mode.)
Startup ranges across runs on one machine: JIT 15.1–20.2 s (it loads about 92 package caches; the high end is a busy machine), sysimage 0.93–2.37 s (the low end with a warm page cache), AOT 0.58–1.97 s (cold-to-warm page cache on the library).
What changes and what does not¶
Startup changes; per-call cost does not. A warm call crosses the same
fcj_* ABI in every mode, so the numbers from
Two SDKs, two speeds hold everywhere: ~815 ns generic,
~3.8 ns on the Fast Path. The capstone’s chemistry totals show it
empirically – 4.79 s (JIT) against 4.02 s (sysimage) over the same 5216
Strang half-steps, a gap that flips sign between runs. It is machine-load
noise (about ±20%), not a mode effect. Pick a mode for startup latency
and for what ships; do not pick it for steady-state speed.
Memory does not change either: the sysimage run’s peak RSS lands within ~30 MB of the JIT run. The image precompiles code; it does not shrink the heap your problem needs.
Mode 1: JIT – the developer default¶
Initialize with julia_init() or julia_init_ex(project_path=...) and
let Julia load precompiled package caches from a depot at runtime. No
extra build steps, every package resolves through its [compat] bounds,
and a top-level julia_eval_string can redefine code live. The price is
the 17.3 s-class startup, and a target machine that carries the whole
depot.
When project_path is omitted, julia_init_ex probes for the
FortranBridge package: the $JULIA_FJ_PROJECT environment variable, then
the copy installed next to the executable, then $JULIA_PROJECT. A
failed probe returns without initializing, the reason in
get_last_error_message().
Mode 2: sysimage – pay at build time, load in two seconds¶
cmake -DFJ_BUILD_SYSIMAGE=ON drives scripts/build_sysimage.jl:
PackageCompiler’s create_sysimage over FortranBridge, SciMLBase,
OrdinaryDiffEq, and FjKinetics, tracing a precompile workload into
the image. The result is a large file – about a gigabyte for this
repository’s SciML-laden image – that the embedded runtime loads at
init. Nothing compiles at runtime, by construction: the code is already
in the image.
The capstone driver switches between the first two modes with one branch,
from examples/07_capstone/1_reacting_flow.f90:
if (trim(jmode) == 'sysimage') then
inquire(file=trim(image_path), exist=lex)
if (.not. lex) then
write (error_unit, '(a)') 'FATAL: sysimage not found at ' // &
trim(image_path) // ' (set FJ_SCIML_SYSIMAGE)'
error stop 1
end if
print '(a,a,a,a)', 'detonation: julia mode=sysimage image=', trim(image_path), &
' project=', trim(project_path)
call julia_init_ex(image_path=trim(image_path), project_path=trim(project_path))
else
print '(a,a,a)', 'detonation: julia mode=jit project=', trim(project_path)
call julia_init_ex(project_path=trim(project_path))
end if
Two caveats. The image is baked for cpu_target="native" – the build
host’s instruction set – so it is a single-host artifact. And it is
pinned to the exact Julia build: the same self-contained 1.12.7 binary
must build the image and embed the runtime; a version shim pointing at
another minor version is rejected by the sysimage build guard
(Installation).
In sysimage mode the image already binds the precompiled modules in
Main, so the runtime import the JIT path needs simply does not run.
The capstone counts 13 init/setup bridge calls in sysimage mode against
15 in JIT, and the difference is that missing import sequence: the
import FjKinetics eval plus the re-probe that follows it.
Mode 3: AOT – Julia as an ordinary shared library¶
Here the direction flips: instead of Fortran embedding Julia,
PackageCompiler’s create_library compiles a Julia package and its whole
dependency closure into dist/, exposing your functions as C symbols.
Fortran links the result like any C library, and the target needs no
Julia install, no depot, no project. Startup is a plain image load
(0.58–1.97 s, cold-to-warm cache), and a first call into the library
answers in ~12 μs. Nothing JITs, ever.
The bridge is not involved in this mode, and that has a sharp edge: the
SDK’s julia_init() must not be used. The interface note from
examples/04_performance/1_jit_vs_aot.f90:
! Lifecycle of the runtime embedded in libmylib.so. The SDK's
! julia_init() must NOT be used in this mode: linking libmylib.so
! makes its bundled libjulia satisfy libjulia.so.1 for the whole
! process, and a bare jl_init() would then look for a default
! sys.so that the bundled layout does not ship (the sysimage is
! compiled INTO libmylib.so and only init_julia() knows how to
! load it, with an explicit bindir).
subroutine init_julia(argc, argv) bind(c, name="init_julia")
use iso_c_binding
integer(c_int), value :: argc
type(c_ptr), value :: argv
end subroutine
subroutine shutdown_julia(retcode) bind(c, name="shutdown_julia")
use iso_c_binding
integer(c_int), value :: retcode
end subroutine
The same first call, both worlds (same source, two builds):
#ifdef COMPILED_MODE
res = my_add_float(10.0d0, 20.0d0)
#else
res_obj = julia%call("perf_add_one", 10.0d0)
res = res_obj%as_real()
#endif
Error handling changes shape too: no Julia exception can cross a C ABI.
The compiled library returns integer codes instead – the capstone’s AOT
chemistry kernel uses 0 for success, -1 for a bad n, -2 for a solver
that was never built, -3 for a solver that threw – and its buffers are
wrapped zero-copy, exactly like the bridge’s fcj_wrap_array.
Two costs to know before choosing it. The build is heavy: the
non-incremental codegen of the whole closure took about 50 min wall and
~33 GB peak compiler RSS for the capstone kernel, and dist/ ships about
2.5 GB. And the API surface freezes: you call fixed C symbols, not names
resolved at runtime.
One sentence of PackageCompiler hygiene: build with
include_lazy_artifacts=true, or the “self-contained” library will
silently download lazy artifacts (MKL among them) from the network the
first time it loads on a machine without a depot.
Working examples: examples/03_deployment/2_compiled_library/ (the small
demo) and examples/07_capstone/3_aot_kinetics/ (the production-shaped
one).
Mode 4: native – the yardstick¶
Pure Fortran, nothing embedded. It belongs in the table because it is the reference: every bridge mode is validated against a native run of the same physics – four acceptance gates, zero deviations, residuals below 1e-12 (Validation). When a workload buys nothing from Julia, native is not a fallback; it is the answer.
Startup control: julia_init_ex¶
All three embedded modes start through the same SDK call, which wraps the
C entry point fcj_init_ex (as julia_shutdown wraps fcj_shutdown).
Its optional arguments are the deployment knobs: image_path (the
sysimage), project_path (the environment), nthreads (Julia threads),
handle_signals (disposition). From
examples/03_deployment/ex_init_options.f90:
! 1. Initialize the Julia runtime with explicit startup options:
! - project_path: omitted on purpose -- the exe-adjacent probe finds
! the FortranBridge package copied next to this executable by the
! build, and an explicit RELATIVE path would be validated against
! the CWD (breaking runs from any other directory)
! - nthreads = 2
! - handle_signals = 0 ("no takeover": SIGINT/SIGTERM are reset to
! SIG_DFL after init; GC-critical handlers stay intact -- bridge.h)
call julia_init_ex(nthreads=2, handle_signals=0)
Failure semantics are deliberate: a bad option (a path too long to fit,
an environment that does not exist) returns without initializing and
get_last_error_message() carries the reason – the process does not
abort. On signals: the bridge never touches jl_options.handle_signals,
because GC safepoint polling depends on Julia’s own handlers; the 0
above means “no takeover”, resetting SIGINT/SIGTERM to their defaults
after init. The legacy julia_init / julia_init_with_image keep their
historical signal behavior.
Choosing¶
- Develop in JIT. Nothing to build, live redefinition, the whole
stack resolvable through
[compat]. The 17.3 s-class startup is a development tax, paid on a machine that can afford it. - Cut startup with a sysimage when the same binary runs repeatedly on a known host. 1.90 s to first result, identical heap footprint, no runtime compilation by construction. Budget the image size, and remember it is host-pinned and Julia-version-pinned.
- Go AOT when the target has no Julia, or when first-call latency is the product. Nothing to install, ~12 μs first call, integer error codes instead of exceptions – and a heavy one-time build you schedule in advance, not stumble into.
- Stay native where Julia buys nothing. The baseline is also a mode.
Where to go next¶
- Bring your own kinetics – the delivery modes wrapped around a real chemistry package.
- Validation – the correctness gates that no mode is allowed to break.
- Performance annex – the full capstone performance report behind every number on this page.