Skip to content

Instantly share code, notes, and snippets.

@Gravifer
Created July 22, 2026 04:27
Show Gist options
  • Select an option

  • Save Gravifer/d5c9e844ba8ea5ded38588ff82092ece to your computer and use it in GitHub Desktop.

Select an option

Save Gravifer/d5c9e844ba8ea5ded38588ff82092ece to your computer and use it in GitHub Desktop.
Range syntax survey

Julia Compilation Latency After the Package-Image Bump

A compact survey and local case study, current through 22 July 2026.

Bottom line

The impression that Julia's compile-time experience has plateaued over the last two to three years is substantially justified—with one qualification. Julia 1.9 and 1.10 greatly improved the reuse and loading of code that package authors anticipated. They did not make arbitrary new application specializations compile an order of magnitude faster.

For compiler-heavy libraries such as Enzyme and Mooncake, a tiny derivative of a new user function can still take roughly ten seconds in every fresh process. The same call is effectively instantaneous once compiled in that process. Julia is therefore much better once warm, while short-lived tools, test processes, and novel function/type combinations continue to encounter a conspicuous cliff.

“Compile time” is three different costs

  1. Installation and artifacts: dependency resolution plus downloads such as LLVM or native libraries.
  2. Package precompilation: inference and native package-image construction, persisted for a particular Julia/package/preferences/CPU configuration.
  3. Application specialization: compilation for concrete functions and types introduced by the user's program. This normally repeats in every new process unless captured in a workload package or sysimage.

Confusing these phases produces misleading conclusions. In particular, --compiled-modules=no disables the cache system and should only be used for diagnosis, never as a representative performance configuration.

Local Julia 1.12.6 measurements

The following are single diagnostic observations on Windows 11 with an Intel Alder Lake CPU, an official Julia 1.12.6 build, pinned package versions, and a workspace-local depot. Artifacts had already been downloaded. Cold times are order-dependent because later packages reused dependencies compiled earlier; these are not cross-package performance rankings.

Package Cold image/load Warm load First user call Same-process repeat Direct image
ForwardDiff 1.4.1 19.31 s 0.129 s 0.870 s 11 µs 3.2 MiB
Zygote 0.7.11 57.59 s 0.886 s 1.767 s 32 µs 27.7 MiB
ReverseDiff 1.17.0 31.35 s 2.820 s 1.840 s 135 µs 19.9 MiB
Mooncake 0.5.40 141.01 s 0.475 s 9.758 s 39.5 ms 72.6 MiB
Enzyme 0.13.190 276.98 s 1.334 s 11.454 s 3 µs 46.3 MiB

First-call compilation allocated approximately 134 MB for ForwardDiff, 193 MB for Zygote, 239 MB for ReverseDiff, 789 MB for Mooncake, and 848 MB for Enzyme.

Five executions of julia --startup-file=no -e '1+1' had a median wall time of 397 ms on this Windows host. That number includes operating-system process startup and should not be compared directly with in-process timings.

What the compiler traces showed

Invalidation was not the main problem in these runs: reported recompilation time was zero or negligible. The cost was fresh specialization tied to the user's function identity and concrete argument types.

  • ForwardDiff emitted 31 timed compilation entries.
  • Zygote emitted 80. Its _pullback specialization for typeof(Main.objective) and Vector{Float64} took about 1.19 seconds.
  • Mooncake emitted 633 entries across compiler inference, Julia-IR conversion, generated AD statements, and construction of the derived rule.
  • Enzyme emitted 327. The outer derivative entry covered 12.31 seconds, including roughly 4.99 seconds in an internal compiler-interpreter inference call.

The timings are nested and cannot be summed. Their importance is qualitative: a library package cannot precompile a derivative for a function that does not exist until the user's program loads. Compiler-based AD magnifies the boundary because it specializes an interpreter or IR transform as well as the numeric kernel and generated derivative.

ReverseDiff displayed a different trade-off. Even with a valid 19.9 MiB package image, its warm import took about 2.8 seconds and allocated roughly 214 MB. Package images remove code generation, but still need to be mapped, deserialized, validated, and integrated with the current method world. More cached code can exchange first-call latency for precompile time, disk space, memory, and warm load work.

What improved, and when

Julia 1.6–1.8: inference and invalidation groundwork

Julia 1.6 substantially reduced method invalidation and added better compiler introspection. Julia 1.8 preserved more inference results across package boundaries. This made later native caching viable.

Sources: Julia 1.6 highlights, Julia 1.8 highlights, analysis of method invalidation.

Julia 1.9: the memorable improvement

Julia 1.9 added native code to package caches. Curated workloads saw TTFX reductions ranging from several-fold to orders of magnitude. The release report also documented the cost: package precompilation became roughly 10–50% slower and caches became larger. PrecompileTools allowed packages to record representative workloads.

Source: Julia 1.9 highlights.

Julia 1.10: loading and parallel cache construction

Julia 1.10 improved warm loading, method-table scaling, invalidation behavior, parallel precompilation-on-import, and coordination between processes. Its artificial 650-package OmniPackage example fell from 48.0 to 19.1 seconds. Parallel LLVM construction of large images was also added, although the release notes state that it is disabled on Windows because of COFF limitations.

Source: Julia 1.10 highlights.

Julia 1.11–1.12: startup, portability, deployment, and visibility

Julia 1.11 moved standard libraries out of the base system image and reported a small-script startup reduction from 113 ms to 92 ms. It also made package caches relocatable, groundwork for distributing prebuilt images.

Julia 1.12 added --trace-compile-timing, @trace_compile, and @trace_dispatch. Optional BOLT-built Julia/LLVM binaries improved compiler-heavy benchmarks around 10%, but the documented BOLT path is Linux-only. Experimental --trim and JuliaC target closed-world deployment with substantial dynamic-dispatch restrictions; they do not remove ordinary interactive JIT work.

Sources: Julia 1.11 highlights, Julia 1.12 highlights.

The overall trajectory since 1.10 has emphasized where compilation happens, how it is cached, and how it can be diagnosed—not another dramatic increase in raw application-specialization throughput.

Why this is hard

Julia deliberately combines open-world method extension, aggressive concrete specialization, and interactive redefinition. Those are central strengths, but they make reusable native code conditional on the method world, preferences, CPU target, and concrete caller types.

That creates several tensions:

  • More precompile coverage reduces future latency but increases package build time and image size.
  • Generic numerical code admits too many combinations of functions, element types, dimensions, AD modes, and backends to cache exhaustively.
  • Interactive method additions require validity and world-age machinery that a conventional closed-world binary can omit.
  • Enzyme, Mooncake, GPUCompiler, and similar systems effectively compile a compiler specialized to each user program.
  • Package images are coarse-grained. Splitting optional functionality improves selectivity but complicates releases and dependency management.

The last pressure is visible in the open Julia monorepo/subpackage discussion. OrdinaryDiffEq achieved sub-second first solves with package images, but broad precompilation approached an hour before tuning; its practical response was to split one large package into dozens of smaller ones.

What helps now

  • Keep sessions alive. A persistent REPL plus Revise is still the most effective general mitigation. The same-process results above show why.

  • Precompile the application boundary. A project-specific package using PrecompileTools.@compile_workload can cache representative user workflows, including derivative calls. See the current PrecompileTools guide.

  • Build a versioned sysimage for stable applications. PackageCompiler can capture package loading and exact first-use paths. Embedded package versions take precedence over the active environment, so images should be treated as explicit build artifacts. See PackageCompiler's sysimage documentation.

  • Reduce cache scope during development. Selected PrecompileTools workloads can be disabled through Preferences and restored for releases or CI.

  • Minimize hard dependencies. Use extensions and smaller packages so users only construct and load images for functionality they need.

  • Measure the right phase. On Julia 1.12:

    using InteractiveUtils
    @time_imports using MyPackage
    @trace_compile my_first_workload()
    @trace_dispatch my_first_workload()

Use SnoopCompile if invalidation or inference is suspected. In the AD case study, timed compile tracing showed that novel user-function specialization—not recompilation—dominated.

What to watch

Work underway during the Julia 1.13 development cycle includes an API for non-native compilers such as GPUCompiler to retain owned CodeInstances during precompilation, more AOT threading, LLVM time-trace integration, and detachable background package precompilation. Persisting non-native compiler results could directly help Enzyme-like systems. Backgrounding improves responsiveness but does not reduce total work.

Sources: January 2026 Julia development summary, February 2026 summary.

Assessment

Julia is much better than its pre-1.9 self at reusing work anticipated by package authors. That is a genuine accomplishment. The complaint survives at the application boundary: novel specializations, particularly transformations of arbitrary user programs, still compile slowly enough to make short-lived Julia processes unattractive.

The next convincing leap likely requires substantially faster inference/codegen, portable served application images, finer-grained reuse across caller boundaries, or a practical closed-world mode that preserves enough of Julia's generic model. Julia 1.12 and the 1.13 development cycle contain pieces of those directions, but none yet eliminates the first-call cliff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment