You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This allows one to monitor invalidations and recompile them.
The main targets are "Startup" packages that load other packages that
may not know about one another, although there does not appear
to be any reason that "regular" packages couldn't use it too.
This also enhances the didactic portions of the documentation,
most notably by adding an explanation of invalidation.
Julia itself has a function `precompile`, to which you can pass specific signatures to force precompilation.
4
+
For example, `precompile(foo, (ArgType1, ArgType2))` will precompile `foo(::ArgType1, ::ArgType2)`*and all of its inferrable callees*.
5
+
Alternatively, you can just execute some code at "top level" within the module, and during precompilation any method or signature "owned" by your package will also be precompiled.
6
+
Thus, base Julia itself has substantial facilities for precompiling code.
7
+
8
+
## The `workload` macros
9
+
10
+
`@compile_workload` adds one key feature: the *non-inferrable callees* (i.e., those called via runtime dispatch) that get
11
+
made inside the `@compile_workload` block will also be cached, *regardless of module ownership*. In essence, it's like you're adding
12
+
an explicit `precompile(noninferrable_callee, (OtherArgType1, ...))` for every runtime-dispatched call made inside `@compile_workload`.
13
+
14
+
These `workload` macros add other features as well:
15
+
16
+
- Statements that occur inside a `@compile_workload` block are executed only if the package is being actively precompiled; it does not run when the package is loaded, nor if you're running Julia with `--compiled-modules=no`.
17
+
- Compared to just running some workload at top-level, `@compile_workload` ensures that your code will be compiled (it disables the interpreter inside the block)
18
+
- PrecompileTools also defines `@setup_workload`, which you can use to create data for use inside a `@compile_workload` block. Like `@compile_workload`, this code only runs when you are precompiling the package, but it does not necessarily result in the `@setup_workload` code being stored in the package precompile file.
19
+
20
+
## `@recompile_invalidations`
21
+
22
+
`@recompile_invalidations` activates logging of invalidations before executing code in the block.
23
+
It then parses the log to extract the "leaves" of the trees of invalidations, which generally represent
24
+
the top-level calls (typically made by runtime dispatch). It then triggers their recompilation.
25
+
Note that the recompiled code may return different results than the original (this possibility is
26
+
why the code had to be invalidated in the first place).
Copy file name to clipboardExpand all lines: docs/src/index.md
+78-18Lines changed: 78 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,8 +12,10 @@ The main tool in `PrecompileTools` is a macro, `@compile_workload`, which precom
12
12
It also includes a second macro, `@setup_workload`, which can be used to "mark" a block of code as being relevant only
13
13
for precompilation but which does not itself force compilation of `@setup_workload` code. (`@setup_workload` is typically used to generate
14
14
test data using functions that you don't need to precompile in your package.)
15
+
Finally, `PrecompileTools` includes `@recompile_invalidations` to mitigate the undesirable consequences of *invalidations*.
16
+
These different tools are demonstrated below.
15
17
16
-
## Tutorial
18
+
## Tutorial: forcing precompilation with workloads
17
19
18
20
No matter whether you're a package developer or a user looking to make your own workloads start faster,
19
21
the basic workflow of `PrecompileTools` is the same.
@@ -139,6 +141,81 @@ All the packages will be loaded, together with their precompiled code.
139
141
!!! tip
140
142
If desired, the [Reexport package](https://github.com/simonster/Reexport.jl) can be used to ensure these packages are also exported by `Startup`.
141
143
144
+
## Tutorial: "healing" invalidations
145
+
146
+
Julia sometimes *invalidates* previously compiled code (see [Why does Julia invalidate code?](@ref)).
147
+
PrecompileTools provides a mechanism to recompile the invalidated code so that you get the full benefits
148
+
of precompilation. This capability can be used in "Startup" packages (like the one described
149
+
above), as well as by package developers.
150
+
151
+
!!! tip
152
+
Excepting [piracy](https://docs.julialang.org/en/v1/manual/style-guide/#Avoid-type-piracy) (which is heavily discouraged),
153
+
*type-stable (i.e., well-inferred) code cannot be invalidated.* If invalidations are a problem, an even better option
154
+
than "healing" the invalidations is improving the inferrability of the "victim": not only will you prevent
155
+
invalidations, you may get faster performance and slimmer binaries. Packages that can help identify
156
+
inference problems and invalidations include [SnoopCompile](https://github.com/timholy/SnoopCompile.jl),
157
+
[JET](https://github.com/aviatesk/JET.jl), and [Cthulhu](https://github.com/JuliaDebug/Cthulhu.jl).
158
+
159
+
The basic usage is simple: wrap expressions that might invalidate with `@recompile_invalidations`.
160
+
Invalidation can be triggered by defining new methods of external functions, including during
161
+
package loading. Using the "Startup" package above, you might wrap the `using` statements:
162
+
163
+
```julia
164
+
module Startup
165
+
166
+
using PrecompileTools
167
+
@recompile_invalidationsbegin
168
+
using LotsOfPackages...
169
+
end
170
+
171
+
# Maybe a @compile_workload here?
172
+
173
+
end
174
+
```
175
+
176
+
Note that recompiling invalidations can be useful even if you don't add any additional workloads.
177
+
178
+
Alternatively, if you're a package developer worried about "collateral damage" you may cause by extending functions
179
+
owned by Base or other package (i.e., those that require `import` or module-scoping when defining the method),
180
+
you can wrap those method definitions:
181
+
182
+
```julia
183
+
module MyContainers
184
+
185
+
using AnotherPackage
186
+
using PrecompileTools
187
+
188
+
struct Container
189
+
list::Vector{Any}
190
+
end
191
+
192
+
# This is a function created by this package, so it doesn't need to be wrapped
193
+
make_container() =Container([])
194
+
195
+
@recompile_invalidationsbegin
196
+
# Only those methods extending Base or other packages need to go here
197
+
Base.push!(obj::Container, x) =...
198
+
function AnotherPackage.foo(obj::Container)
199
+
⋮
200
+
end
201
+
end
202
+
203
+
end
204
+
```
205
+
206
+
You can have more than one `@recompile_invalidations` block in a module. For example, you might use one to wrap your
207
+
`using`s, and a second to wrap your method extensions.
208
+
209
+
!!! warning
210
+
Package developers should be aware of the tradeoffs in using `@recompile_invalidations` to wrap method extensions:
211
+
212
+
- the benefit is that you might deliver a better out-of-the-box experience for your users, without them needing to customize anything
213
+
- the downside is that it will increase the precompilation time for your package. Worse, what can be invalidated once can sometimes be invalidated again by a later package, and if that happens the time spent recompiling is wasted.
214
+
215
+
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.
216
+
217
+
Package developers are encouraged to try to fix "known" invalidations rather than relying reflexively on `@recompile_invalidations`.
218
+
142
219
## When you can't run a workload
143
220
144
221
There are cases where you might want to precompile code but cannot safely *execute* 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.
Julia itself has a function `precompile`, to which you can pass specific signatures to force precompilation.
205
-
For example, `precompile(foo, (ArgType1, ArgType2))` will precompile `foo(::ArgType1, ::ArgType2)`*and all of its inferrable callees*.
206
-
Alternatively, you can just execute some code at "top level" within the module, and during precompilation any method or signature "owned" by your package will also be precompiled.
207
-
Thus, base Julia itself has substantial facilities for precompiling code.
208
-
209
-
`@compile_workload` adds one key feature: the *non-inferrable callees* (i.e., those called via runtime dispatch) that get
210
-
made inside the `@compile_workload` block will also be cached, *regardless of module ownership*. In essence, it's like you're adding
211
-
an explicit `precompile(noninferrable_callee, (OtherArgType1, ...))` for every runtime-dispatched call made inside `@compile_workload`.
212
-
213
-
`PrecompileTools` adds other features as well:
214
-
215
-
- Statements that occur inside a `@compile_workload` block are executed only if the package is being actively precompiled; it does not run when the package is loaded, nor if you're running Julia with `--compiled-modules=no`.
216
-
- Compared to just running some workload at top-level, `@compile_workload` ensures that your code will be compiled (it disables the interpreter inside the block)
217
-
- PrecompileTools also defines `@setup_workload`, which you can use to create data for use inside a `@compile_workload` block. Like `@compile_workload`, this code only runs when you are precompiling the package, but it does not necessarily result in the `@setup_workload` code being stored in the package precompile file.
Julia may be unique among computer languages in supporting all four of the following features:
4
+
5
+
1. interactive development
6
+
2. "method overloading" by packages that don't own the function
7
+
3. aggressive compilation
8
+
4. consistent compilation: same result no matter how you got there
9
+
10
+
The combination of these features *requires* that you sometimes "throw away" code that you have previously compiled.
11
+
12
+
To illustrate: suppose you have a function `numchildren` with one method,
13
+
14
+
```
15
+
numchildren(::Any) = 1
16
+
```
17
+
18
+
and then write
19
+
20
+
```
21
+
total_children(list) = sum(numchildren.(list))
22
+
```
23
+
24
+
Now let `list` be a `Vector{Any}`. You can compile a fast `total_children(::Vector{Any})` (*aggressive compilation*) by leveraging the fact that you know there's only one possible method of `numchildren`, and you know that it returns 1
25
+
for every input. Thus, `total_children(list)` gives you just `length(list)`, which would indeed be a very highly-optimized implementation!
26
+
27
+
But now suppose you add a second method (*interactive development* + *method overloading*)
28
+
29
+
```
30
+
numchildren(::BinaryNode) = 2
31
+
```
32
+
33
+
where `BinaryNode` is a new type you've defined (so it's not type-piracy). If you want to get the right answer (*consistent compilation*) from an arbitrary `list::Vector{Any}`, there are only two options:
34
+
35
+
> **Option A**: plan for this eventuality from the beginning, by making every `numchildren(::Any)` be called by runtime dispatch. But when there is only one method of `numchildren`, forcing runtime dispatch makes the code vastly slower. Thus, this option at least partly violates *aggressive compilation*.
36
+
37
+
> **Option B**: throw away the code for `total_children` that you created when there was only one method of `numchildren`, and recompile it in this new world where there are two.
38
+
39
+
Julia does a mix of these: it does **B** up to 3 methods, and then **A** thereafter. (Recent versions of Julia have experimental support for customizing this behavior with `Base.Experimental.@max_methods`.)
40
+
41
+
This example was framed as an experiment at the REPL, but it is also relevant if you load two packages: `PkgX` might define `numchildren` and `total_children`, and `PkgY` might load `PkgX` and define a second method of `PkgX.numchildren`.
42
+
Any precompilation that occurs in `PkgX` doesn't know what's going to happen in `PkgY`.
43
+
Therefore, unless you want to defer *all* compilation, including for Julia itself, until the entire session is loaded and then closed to further extension (similar to how compilers for C, Rust, etc. work), you have to make the same choice between options **A** and **B**.
44
+
45
+
Given that invalidation is necessary if Julia code is to be both fast and deliver the answers you expect, invalidation is a good thing!
46
+
But sometimes Julia "defensively" throws out code that might be correct but can't be proved to be correct by Julia's type-inference machinery; such cases of "spurious invalidation" serve to (uselessly) increase latency and worsen the Julia experience.
47
+
Except in cases of [piracy](https://docs.julialang.org/en/v1/manual/style-guide/#Avoid-type-piracy), invalidation is a risk
48
+
only for poorly-inferred code. With our example of `numchildren` and `total_children` above, the invalidations were necessary because `list` was
49
+
a `Vector{Any}`, meaning that the elements might be of `Any` type and therefore Julia can't predict in advance which
50
+
method(s) of `numchildren` would be applicable. Were one to create `list` as, say, `list = Union{BinaryNode,TrinaryNode}[]` (where `TrinaryNode` is some other kind of object with children), Julia would know much more
51
+
about the types of the objects to which it applies `numchildren`: defining yet another new method like `numchildren(::ArbitraryNode)` would not trigger invalidations of code
52
+
that was compiled for a `list::Vector{Union{BinaryNode,TrinaryNode}}`.
0 commit comments