PrecompileTools
is designed to help reduce delay on first usage of Julia code. It can forceprecompilation of specific workloads; particularly with Julia 1.9 and higher, the precompiled code is automatically saved to disk, so that it doesn't need to be compiled freshly in each Julia session. You can usePrecompileTools
as a package developer, to reduce the latency experienced by users of your package for "typical" workloads; you can also usePrecompileTools
as a user, creating custom "Startup" package(s) that precompile workloads important for your work.
The main tool inPrecompileTools
is a macro,@compile_workload
, which precompiles all the code needed to execute the workload. It also includes a second macro,@setup_workload
, which can be used to "mark" a block of code as being relevant only for precompilation but which does not itself force compilation of@setup_workload
code. (@setup_workload
is typically used to generate test data using functions that you don't need to precompile in your package.) Finally,PrecompileTools
includes@recompile_invalidations
to mitigate the undesirable consequences ofinvalidations. These different tools are demonstrated below.
The latency reductions from PrecompileTools are maximally effective for Julia versions 1.9 and higher, and intermediate for Julia 1.8. Julia versions 1.7 and earlier may see some limited benefit as well, but have also occasionally been found to suffer fromprecompilation-induced runtime performance regressions. If you wish, you can disable precompilation on older Julia versions by wrapping precompilation statements (see below) withif Base.VERSION >= v"1.8" ... end
. On older Julia versions, you may wish to test packages for performance regressions when introducing precompilation directives.
No matter whether you're a package developer or a user looking to make your own workloads start faster, the basic workflow ofPrecompileTools
is the same. Here's an illustration of how you might use@compile_workload
and@setup_workload
:
module MyPackageusing PrecompileTools: @setup_workload, @compile_workload # this is a small dependencystruct MyType x::Intendstruct OtherType str::Stringend@setup_workload begin # Putting some things in `@setup_workload` instead of `@compile_workload` can reduce the size of the # precompile file and potentially make loading faster. list = [OtherType("hello"), OtherType("world!")] @compile_workload begin # all calls in this block will be precompiled, regardless of whether # they belong to your package or not (on Julia 1.8 and higher) d = Dict(MyType(1) => list) x = get(d, MyType(2), nothing) last(d[MyType(1)]) endendend
@compile_workload
should go at "top level," not compiled into a function. A pattern likewithenv(...) do @compile_workload begin ... end end
, where@compile_workload
appears in the anonymous function passed towithenv
, may defeat some of the value of PrecompileTools.
When you buildMyPackage
, it will precompile the following,including all their callees:
Pair(::MyPackage.MyType, ::Vector{MyPackage.OtherType})
Dict(::Pair{MyPackage.MyType, Vector{MyPackage.OtherType}})
get(::Dict{MyPackage.MyType, Vector{MyPackage.OtherType}}, ::MyPackage.MyType, ::Nothing)
getindex(::Dict{MyPackage.MyType, Vector{MyPackage.OtherType}}, ::MyPackage.MyType)
last(::Vector{MyPackage.OtherType})
In this case, the "top level" calls were fully inferrable, so there are no entries on this list that were called by runtime dispatch. Thus, here you could have gotten the same result with manualprecompile
directives. The key advantage of@compile_workload
is that it works even if the functions you're calling have runtime dispatch.
Once you set up a block usingPrecompileTools
, try your package and see if it reduces the time to first execution, using the same workload you put inside the@compile_workload
block.
If you're happy with the results, you're done! If you want deeper verification of whether it worked as expected, or if you suspect problems, theSnoopCompile package provides diagnostic tools. Potential sources of trouble include invalidation (diagnosed withSnoopCompileCore.@snoop_invalidations
and related tools) and omission of intended calls from inside the@compile_workload
block (diagnosed withSnoopCompileCore.@snoop_inference
and related tools).
@compile_workload
works by monitoring type-inference. If the code was already inferred prior to@compile_workload
(e.g., from prior usage), you might omit any external methods that were called via runtime dispatch.
You can use multiple@compile_workload
blocks if you need to interleave@setup_workload
code with code that you want precompiled. You can use@snoop_inference
to check for any (re)inference when you use the code in your package. To fix any specific problems, you can combine@compile_workload
with manualprecompile
directives.
Users who want to precompile workloads that have not been precompiled by the packages they use can follow the recipe above, creating custom "Startup" packages for each project. Imagine that you have three different kinds of analyses you do: you could have a folder
MyData/ Project1/ Project2/ Project3/
From each one of thoseProject
folders you could do the following:
(@v1.9) pkg> activate . Activating new project at `/tmp/Project1`(Project1) pkg> generate Startup Generating project Startup: Startup/Project.toml Startup/src/Startup.jl(Project1) pkg> dev ./Startup Resolving package versions... Updating `/tmp/Project1/Project.toml` [e9c42744] + Startup v0.1.0 `Startup` Updating `/tmp/Project1/Manifest.toml` [e9c42744] + Startup v0.1.0 `Startup`(Project1) pkg> activate Startup/ Activating project at `/tmp/Project1/Startup`(Startup) pkg> add PrecompileTools LotsOfPackages...
In the last step, you addPrecompileTools
and all the package you'll need for your work onProject1
as dependencies ofStartup
. Then edit theStartup/src/Startup.jl
file to look similar to the tutorial previous section, e.g.,
module Startupusing LotsOfPackages...using PrecompileTools@compile_workload begin # inside here, put a "toy example" of everything you want to be fastendend
Then when you're ready to start work, from theProject1
environment just sayusing Startup
. All the packages will be loaded, together with their precompiled code.
If desired, theReexport package can be used to ensure these packages are also exported byStartup
.
Julia sometimesinvalidates previously compiled code (seeWhy does Julia invalidate code?). PrecompileTools provides a mechanism to recompile the invalidated code so that you get the full benefits of precompilation. This capability can be used in "Startup" packages (like the one described above), as well as by package developers.
Exceptingpiracy (which is heavily discouraged),type-stable (i.e., well-inferred) code cannot be invalidated. If invalidations are a problem, an even better option than "healing" the invalidations is improving the inferrability of the "victim": not only will you prevent invalidations, you may get faster performance and slimmer binaries. Packages that can help identify inference problems and invalidations includeSnoopCompile,JET, andCthulhu.
The basic usage is simple: wrap expressions that might invalidate with@recompile_invalidations
. Invalidation can be triggered by defining new methods of external functions, including during package loading. Using the "Startup" package above, you might wrap theusing
statements:
module Startupusing PrecompileTools@recompile_invalidations begin using LotsOfPackages...end# Maybe a @compile_workload here?end
Note that recompiling invalidations can be useful even if you don't add any additional workloads.
Alternatively, if you're a package developer worried about "collateral damage" you may cause by extending functions owned by Base or other package (i.e., those that requireimport
or module-scoping when defining the method), you can wrap those method definitions:
module MyContainersusing AnotherPackageusing PrecompileToolsstruct Container list::Vector{Any}end# This is a function created by this package, so it doesn't need to be wrappedmake_container() = Container([])@recompile_invalidations begin # Only those methods extending Base or other packages need to go here Base.push!(obj::Container, x) = ... function AnotherPackage.foo(obj::Container) ⋮ endendend
You can have more than one@recompile_invalidations
block in a module. For example, you might use one to wrap yourusing
s, and a second to wrap your method extensions.
Package developers should be aware of the tradeoffs in using@recompile_invalidations
to wrap method extensions:
Using@recompile_invalidations
in a "Startup" package is, in a sense, safer because it waits for all the code to be loaded before recompiling anything. On the other hand, this requires users to implement their own customizations.
Package developers are encouraged to try to fix "known" invalidations rather than relying reflexively on@recompile_invalidations
.
There are cases where you might want to precompile code but cannot safelyexecute that code: for example, you may need to connect to a database, or perhaps this is a plotting package but you may be currently on a headless server lacking a display, etc. In that case, your best option is to fall back on Julia's ownprecompile
function. However, as explained inHow PrecompileTools works, there are some differences betweenprecompile
and@compile_workload
; most likely, you may need multipleprecompile
directives. Analysis withSnoopCompile may be required to obtain the results you want; in particular, combining@snoop_inference
andparcel
will allow you to generate a set ofprecompile
directives that can beinclude
d in your module definition.
Be aware thatprecompile
directives are more specific to the Julia version, CPU (integer width), and OS than running a workload.
Ensure your workload "works" (runs without error) when copy/pasted into the REPL. If it produces an error only when placed inside@precompile_workload
, check whether your workload runs when wrapped in a
let # workload goes hereend
block.
If you're frequently modifying one or more packages, you may not want to spend the extra time precompiling the full set of workloads that you've chosen to make fast for your "shipped" releases. One canlocally reduce the cost of precompilation for selected packages using thePreferences.jl
-based mechanism and the"precompile_workload"
key: from within your development environment, use
using MyPackage, Preferencesset_preferences!(MyPackage, "precompile_workload" => false; force=true)
This will write the following to LocalPreferences.toml alongside your active environment Project.toml
[MyPackage]precompile_workload = false
After restarting julia, the@compile_workload
and@setup_workload
workloads will be disabled (locally) forMyPackage
. You can also specify additional packages (e.g., dependencies ofMyPackage
) if you're co-developing a suite of packages. Simply runset_preferences!
for the additional packages, or edit LocalPreferences.toml directly.
Changingprecompile_workload
will result in a one-time recompilation of all packages that depend on the package(s) from the current environment. Package developers may wish to set this preference locally within the "main" package's environment; precompilation will be skipped while you're actively developing the project, but not if you use the package from an external environment. This will also keep theprecompile_workload
setting independent and avoid needless recompilation of large environments.
Finally, it is possible to fully disable PrecompileTools.jl for all packages with
using PrecompileTools, Preferencesset_preferences!(PrecompileTools, "precompile_workloads" => false; force=true)
This can be helpful to reduce the system image size generated when using PackageCompiler.jl by only compiling calls made in a precompilation script.
If you want to see the list of calls that will be precompiled, navigate to theMyPackage
folder and use
julia> using PrecompileToolsjulia> PrecompileTools.verbose[] = true # runs the block even if you're not precompiling, and print precompiled callsjulia> include("src/MyPackage.jl");
This will only show the direct- or runtime-dispatched method instances that got precompiled (omitting their inferrable callees). For a more comprehensive list of all items stored in the compile_workload file, seePkgCacheInspector.
Settings
This document was generated withDocumenter.jl version 1.9.0 onThursday 10 April 2025. Using Julia version 1.13.0-DEV.371.