PrecompileTools.jl
repository·main·Indexed 42 days ago
https://github.com/julialang/precompiletools.jlA utility for the Julia ecosystem designed to minimize the latency of the first execution of code (Time to First Execution, or TTFX). It provides macros like `@compile_workload` to cache runtime-dispatched calls, `@setup_workload` to prepare data for compilation, and `@recompile_invalidations` to restore performance after code invalidation. It is intended for package developers to improve the out-of-the-box user experience and for users to optimize personal workflows via custom Startup packages.
What's inside PrecompileTools
- PrecompileTools is a tool designed to reduce the latency of the first execution of Julia code (Time to First Execution, or TTFX). It is intended for both package developers, to improve the out-of-the-box experience for their users, and for ordinary users to optimize their personal workflows.
Understanding Julia code invalidation
mainIn Julia, code invalidation is the process of throwing away previously compiled code to ensure correctness when new methods are added to a function. This is necessary because Julia uses aggressive compilation to optimize code based on the current set of available methods.
Why invalidation occurs
Invalidation is triggered by the combination of:
- Interactive development: Adding new code during a session.
- Method overloading: Packages adding methods to functions they do not own.
- Aggressive compilation: Optimizing code by assuming only certain methods are available (e.g., specializing a function because only one method of a called function exists).
- Consistent compilation: Ensuring the same result regardless of the order in which code was loaded.
The trade-off: Aggressive vs. Runtime Dispatch
When a new method is added to a function that was previously used in optimized code, Julia has two choices:
- Option A (Runtime Dispatch): Always use runtime dispatch. This is slower but avoids invalidation.
- Option B (Recompilation): Throw away the optimized code and recompile it to account for the new method. This maintains high performance.
Julia typically follows Option B for up to 3 methods and then switches to Option A. Recent Julia versions offer experimental control over this via
Base.Experimental.@max_methods.Avoiding Spurious Invalidation
"Spurious invalidation" occurs when Julia defensively throws out code that is actually correct but cannot be proven correct by the type-inference machinery. This is most common with poorly-inferred code (e.g., using
Anytypes).To minimize unnecessary invalidations, use more specific types instead of
Any. For example:- Using
Vector{Any}makes it impossible for Julia to predict which methods of a called function will apply, triggering invalidation when new methods are added. - Using a specific
Uniontype, such asVector{Union{BinaryNode, TrinaryNode}}, allows Julia to track applicable methods more accurately and avoid invalidating code when a third type (likeArbitraryNode) is added.
Compare PrecompileTools and PackageCompiler
mainWhile both tools aim to reduce Time to First Execution (TTFX), they serve different primary purposes.
- PrecompileTools is ideal for package developers to ensure users have a fast experience immediately after installation without needing to rebuild Julia. It allows packages to be updated without a full system image rebuild.
- PackageCompiler is focused on creating system images that dramatically speed up the time to load (
using ...) by skipping code-validation checks, but it requires rebuilding the system image whenever packages change.
Task Julia 1.9 + PrecompileTools PackageCompiler Developers can reduce out-of-box TTFX for their users ✔️ ❌ Users can reduce TTFX for custom tasks ✔️ ✔️ Packages can be updated without rebuilding system image ✔️ ❌ Reduces time to load (TTL) ❌ ✔️ Handle workloads that cannot be executed safely
mainIf you need to precompile code that requires external resources (e.g., a database connection or a GUI/display) that cannot be safely executed during precompilation, you cannot use
@compile_workload.Alternative: Manual
precompiledirectivesFall back to Julia's built-in
precompilefunction. Because@compile_workloadandprecompilebehave differently, you may need multiple manualprecompiledirectives.To generate these directives, it is recommended to use
SnoopCompile(specifically combining@snoop_inferenceandparcel) to obtain a set ofprecompiledirectives that can beincluded in your module definition.Force precompilation with @compile_workload and @setup_workload
mainUse
PrecompileToolsto reduce latency on first usage of Julia code by forcing the precompilation of specific workloads. This is effective for Julia 1.9+ (automatic disk saving) and 1.8 (limited benefit).Macros
@compile_workload:- Precompiles all code needed to execute the block, including all callees.
- Works even if functions involve runtime dispatch.
- Important: Must be used at the "top level", not inside a function or an anonymous function (e.g., avoid wrapping it inside
withenv(...) do ... end).
@setup_workload:- Marks a block of code as relevant for precompilation without forcing immediate compilation.
- Useful for generating test data or setup objects that you want to be available during the precompilation process but don't want to include in the final precompile file (which keeps the file size smaller and loading faster).
Best Practices
- You can interleave multiple
@compile_workloadblocks with@setup_workloadcode. - If the code was already inferred prior to the macro (e.g., from prior usage), you might omit external methods called via runtime dispatch. Use
SnoopCompileto verify.
module MyPackage using PrecompileTools: @setup_workload, @compile_workload struct MyType x::Int end struct OtherType str::String end @setup_workload begin # Setup code (e.g., generating data) that doesn't need to be in the precompile file list = [OtherType("hello"), OtherType("world!")] @compile_workload begin # Code that MUST be precompiled d = Dict(MyType(1) => list) x = get(d, MyType(2), nothing) last(d[MyType(1)]) end end endCreate local "Startup" packages to precompile custom workloads
mainUsers can create custom "Startup" packages to precompile workloads that their existing packages haven't precompiled. This is useful for specific projects or analysis environments.
Workflow
- Activate a project:
pkg> activate . - Generate a Startup package:
pkg> generate Startup - Develop the Startup package:
pkg> dev ./Startup - Configure dependencies: Activate the
Startupproject and addPrecompileToolsalong with all packages required for your workload. - Define workloads: In
Startup/src/Startup.jl, use@compile_workloadto include "toy examples" of the code you want to be fast. - Use it: In your main project, simply call
using Startupto load all packages and their precompiled code.
Tip: Use the
Reexport.jlpackage within yourStartupmodule to ensure all dependencies are also exported byStartup.# Inside Startup/src/Startup.jl module Startup using LotsOfPackages... using PrecompileTools @compile_workload begin # put a "toy example" of everything you want to be fast end end- Activate a project:
Reduce precompilation cost during development
mainIf you are frequently modifying a package, you can locally disable its
@compile_workloadand@setup_workloadto speed up your development cycle. This is done usingPreferences.jlby setting theprecompile_workloadkey tofalse. This setting is stored inLocalPreferences.tomland only affects your local development environment.Note: Changing this preference will trigger a one-time recompilation of all packages that depend on the modified package in your current environment.
using MyPackage, Preferences set_preferences!(MyPackage, "precompile_workload" => false; force=true)Inspect precompiled calls in verbose mode
mainTo see a list of the direct or runtime-dispatched method instances that are being precompiled, you can enable verbose mode in
PrecompileTools. This will print the precompiled calls to the console even if you are not currently performing a formal precompilation step.using PrecompileTools PrecompileTools.verbose[] = true include("src/MyPackage.jl")Heal invalidations with @recompile_invalidations
mainWhen Julia invalidates previously compiled code (often due to method extensions),
PrecompileToolsprovides@recompile_invalidationsto recompile that code and restore performance.Usage Patterns
- In Startup packages: Wrap
usingstatements to ensure all packages are loaded before recompiling invalidations. - In Package development: Wrap method extensions that target
Baseor other packages to ensure users get a better out-of-the-box experience.
Trade-offs for Package Developers
- Pros: Delivers a better experience for users without requiring them to customize anything.
- Cons: Increases package precompilation time. Furthermore, what is invalidated once might be invalidated again by a later package, wasting the recompilation time.
Note: It is generally better to fix the underlying cause of invalidation (improving inferrability) using tools like
SnoopCompile,JET, orCthulhurather than relying on@recompile_invalidations.# Example 1: In a Startup package module Startup using PrecompileTools @recompile_invalidations begin using LotsOfPackages... end end # Example 2: In a package extending other packages module MyContainers using AnotherPackage using PrecompileTools struct Container list::Vector{Any} end @recompile_invalidations begin Base.push!(obj::Container, x) = ... function AnotherPackage.foo(obj::Container) # ... end end end- In Startup packages: Wrap
Disable PrecompileTools.jl for all packages
mainTo fully disable
PrecompileTools.jlfor all packages, you can set theprecompile_workloadspreference tofalse. This is useful when usingPackageCompiler.jlif you want to reduce the system image size by only compiling calls made specifically within a precompilation script.using PrecompileTools, Preferences set_preferences!(PrecompileTools, "precompile_workloads" => false; force=true)Troubleshoot @compile_workload errors
mainIf your workload fails when placed inside
@compile_workload, follow these steps:- Verify in REPL: Ensure the workload runs without error when copy-pasted directly into the REPL.
- Test with
letblock: If it fails in the macro but works in the REPL, check if the workload runs successfully when wrapped in aletblock:let # workload goes here end
Trigger recompilation of invalidated code with `@recompile_invalidations`
mainThe
@recompile_invalidationsmacro is used to ensure that code invalidated by runtime dispatch is properly recompiled.It works by:
- Activating logging of invalidations.
- Executing the code within the block.
- Parsing the log to identify the "leaves" of the invalidation trees (which typically represent the top-level calls made via runtime dispatch).
- Triggering recompilation for those specific leaves.
Warning: The recompiled code may return different results than the original code; this is the fundamental reason why the invalidation occurred.
@recompile_invalidations begin # Code that might trigger invalidations some_runtime_dispatch_function(arg) end