Skip to content

Commit 0ea2829

Browse files
docs: explain what world-age issue is (#362)
Co-authored-by: Tim Holy <[email protected]>
1 parent f0e5b95 commit 0ea2829

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ makedocs(
1212
"registry.md",
1313
"registering.md",
1414
"implementing.md",
15+
"world_age_issue.md",
1516
"reference.md",
1617
],
1718
)

docs/src/world_age_issue.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# World age issue
2+
3+
## Motivation: lazy loading
4+
5+
The goal of FileIO is to provide a unified IO frontend so that users can easily deal with file IO
6+
with the simple `load`/`save` functions. The actual IO work will be dispatched to various IO
7+
backends. For instance, [PNGFiles.jl](https://github.com/JuliaIO/PNGFiles.jl) is used to load PNG
8+
format images. If `using FileIO` were to load all registered IO backends, then it would be very slow
9+
to load, hurting all users of FileIO. For any given user, most of those backends would also be
10+
unnecessary -- for example, people who don't do image processing probably don't want to load any
11+
thing related to image IO.
12+
13+
To avoid such unnecessary loading latency, FileIO defers package loading until it's actually used.
14+
For instance, when you use FileIO, you'll probably observe something like this:
15+
16+
```julia
17+
julia> using TestImages, FileIO
18+
19+
julia> path = testimage("cameraman"; download_only=true)
20+
"/home/jc/.julia/artifacts/27a4c26bcdd47eb717bee089ec231a899cb8ef69/cameraman.tif"
21+
22+
julia> load(path) # actual backend loading happens here
23+
[ Info: Precompiling ImageIO [82e4d734-157c-48bb-816b-45c225c6df19]
24+
[ Info: Precompiling TiffImages [731e570b-9d59-4bfa-96dc-6df516fadf69]
25+
...
26+
```
27+
28+
ImageIO and TiffImages were loaded because the file in `path` was detected to be a TIFF image, well
29+
after FileIO was loaded into the session.
30+
31+
## The hidden issue
32+
33+
Although this lazy-loading trick reduces the time needed for `using FileIO`, it isn't normal
34+
practice in Julia because it introduces a so-called _world age issue_ or _world age problem_. The
35+
world age issue happens when you call methods that get compiled in a newer "world" (get compiled
36+
after initial compilation finishes) than the one you called them from.
37+
38+
Let's demonstrate the problem concretely. In case you don't have a suitable file to play with, let's
39+
first create one:
40+
41+
```julia
42+
julia> using IndirectArrays, ImageCore
43+
44+
julia> img = IndirectArray(rand(1:5, 4, 4), rand(RGB, 5))
45+
4×4 IndirectArray{RGB{Float64}, 2, Int64, Matrix{Int64}, Vector{RGB{Float64}}}:
46+
...
47+
48+
julia> save("indexed_image.png", img)
49+
```
50+
51+
Now, **reopen a new julia REPL** (this is crucial for demonstrating the problem) and call `load`
52+
from **within a function** (this is also crucial):
53+
54+
```julia
55+
julia> using FileIO
56+
57+
julia> f() = size(load("indexed_image.png"))
58+
f (generic function with 1 method)
59+
60+
julia> f()
61+
ERROR: MethodError: no method matching size(::IndirectArrays.IndirectArray{ColorTypes.RGB{FixedPointNumbers.N0f8}, 2, UInt8, Matrix{UInt8}, OffsetArrays.OffsetVector{ColorTypes.RGB{FixedPointNumbers.N0f8}, Vector{ColorTypes.RGB{FixedPointNumbers.N0f8}}}})
62+
The applicable method may be too new: running in world age 32382, while current world is 32416.
63+
Closest candidates are:
64+
size(::IndirectArrays.IndirectArray) at ~/.julia/packages/IndirectArrays/BUQO3/src/IndirectArrays.jl:52 (method too new to be called from this world context.)
65+
size(::AbstractArray{T, N}, ::Any) where {T, N} at abstractarray.jl:42
66+
size(::Union{LinearAlgebra.Adjoint{T, var"#s880"}, LinearAlgebra.Transpose{T, var"#s880"}} where {T, var"#s880"<:(AbstractVector)}) at /Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/LinearAlgebra/src/adjtrans.jl:173
67+
...
68+
Stacktrace:
69+
[1] f()
70+
@ Main ./REPL[2]:1
71+
[2] top-level scope
72+
@ REPL[3]:1
73+
```
74+
75+
To understand why this happened, you have to understand the order of events:
76+
77+
1. When calling `f()` from the REPL, Julia first compiled `f`. Importantly, when compiling, Julia
78+
didn't know what type of object was going to be returned by `load`, so in the compiled code it
79+
waits to see what object actually gets returned before figuring out which method of `size` to
80+
call. (This is called _runtime dispatch_.)
81+
2. It queried the file, recognized a PNG file, and _loaded the ImageIO and PNGFiles packages_. (It's
82+
for the loading of these packages that you needed to start a fresh Julia session.)
83+
3. FileIO calls the appropriate PNG-specific `load` function in PNGFiles. (We'll have more to say
84+
about this step further below.) This causes an image to be returned, which is an array of a type
85+
defined by the IndirectArrays package (a dependency of PNGFiles).
86+
4. `f` calls `size` on the returned image. However, this fails, because at the time you called `f`,
87+
the IndirectArrays package wasn't loaded.
88+
89+
In other words, `size` method for `IndirectArray` lives in a world that's newer than the one from
90+
which you called `f()`. This leads to the observed error.
91+
92+
!!! note
93+
World age is crucial to Julia's ability to allow you to _redefine_ methods interactively, but
94+
the error we're illustrating is an unfortunate side-effect.
95+
96+
The good news is it's easy to fix, just try calling `f()` again:
97+
98+
```julia
99+
julia> f()
100+
(4, 4)
101+
```
102+
103+
The second `f()` works because this time you're calling `f()` in the latest world age with the
104+
necessary `size(::IndirectArray)` already defined. In essence, you fast-forward to the latest world
105+
with each statement you type at the REPL.
106+
107+
## Solutions
108+
109+
### `Base.invokelatest`
110+
111+
One solution is to make the call to `size` via `Base.invokelatest`, which exists explicitly to work
112+
around this world-age dispatch problem. Literally, `invokelatest` dispatches the supplied call using
113+
the latest world age (which may be newer than when you typed `f()` at the REPL). **In a fresh Julia
114+
session**,
115+
116+
```julia
117+
julia> using FileIO
118+
119+
julia> f() = Base.invokelatest(size, load("indexed_image.png"))
120+
f (generic function with 1 method)
121+
122+
julia> f()
123+
(4, 4)
124+
```
125+
126+
!!! note
127+
In step 3 above ("FileIO calls the appropriate PNG-specific `load` function in PNGFiles"),
128+
the call to the `load` function defined in PNGFiles is made via `invokelatest`.
129+
Otherwise, even ordinary interactive usage of FileIO (without burying `load` inside a function)
130+
would cause world-age errors.
131+
132+
!!! warning
133+
Using `invokelatest` slows your code considerably. Use it only when absolutely necessary.
134+
135+
### Eagerly load the required packages first
136+
137+
Another solution to the world age issue is simple and doesn't have long-term downsides: **eagerly
138+
load the needed packages**. For instance, if you're seeing world age issue complaining methods
139+
related to `IndirectArray`, then load IndirectArrays eagerly:
140+
141+
```julia
142+
julia> using FileIO, IndirectArrays # try this on a new Julia REPL
143+
144+
julia> f() = size(load("indexed_image.png"))
145+
f (generic function with 1 method)
146+
147+
julia> f()
148+
(4, 4)
149+
```
150+
151+
Thus if you want to build a package, it could be something like this:
152+
153+
```julia
154+
module MyFancyPackage
155+
156+
# This ensures that whoever loads `MyFancyPackage`, he has IndirectArrays loaded and
157+
# thus avoid the world age issue.
158+
using IndirectArrays, FileIO
159+
160+
f(file) = length(load(file))
161+
end
162+
```
163+
164+
Enjoy the FileIO and its lazy loading, but be aware that its speedy loading comes with some caveats.

0 commit comments

Comments
 (0)