Проблема «возраста мира»

Назначение: отложенная загрузка

Целью пакета FileIO является предоставление унифицированного интерфейса ввода-вывода, чтобы пользователи могли легко работать с файловым вводом-выводом с помощью простых функций load и save. Сами операции ввода-вывода будут диспетчеризоваться в различные бэкенды ввода-вывода. Например, для загрузки изображений в формате PNG применяется PNGFiles.jl. Если бы при выполнении инструкции using FileIO загружались все зарегистрированные бэкенды ввода-вывода, это происходило бы очень медленно, что затрагивало бы всех пользователей FileIO. Кроме того, каждому конкретному пользователю большинство этих бэкендов были бы не нужны. Например, если пользователь не занимается обработкой изображений, ему, вероятно, не требуется загружать бэкенды, связанные с их вводом-выводом.

Во избежание таких лишних задержек FileIO откладывает загрузку пакетов до тех пор, пока они не потребуются. Например, при использовании FileIO вы можете увидеть нечто подобное:

julia> using TestImages, FileIO

julia> path = testimage("cameraman"; download_only=true)
"/home/jc/.julia/artifacts/27a4c26bcdd47eb717bee089ec231a899cb8ef69/cameraman.tif"

julia> load(path) # загрузка бэкенда на самом деле происходит здесь
[ Info: Precompiling ImageIO [82e4d734-157c-48bb-816b-45c225c6df19]
[ Info: Precompiling TiffImages [731e570b-9d59-4bfa-96dc-6df516fadf69]
...

Бэкенды ImageIO и TiffImages были загружены, потому что файл в path был распознан как изображение TIFF, но произошло это значительно позже загрузки FileIO в сеанс.

Скрытая проблема

Хотя прием отложенной загрузки существенно ускоряет выполнение using FileIO, он не является общепринятым в Julia, потому что влечет за собой так называемую проблему «возраста мира». Проблема «возраста мира» возникает, когда вы вызываете методы, которые компилируются в более новом «мире» (после завершения начальной компиляции), чем тот, из которого они вызываются.

Давайте продемонстрируем это на примере. Ели у вас нет подходящего файла для экспериментов, сначала создадим его:

julia> using IndirectArrays, ImageCore

julia> img = IndirectArray(rand(1:5, 4, 4), rand(RGB, 5))
4×4 IndirectArray{RGB{Float64}, 2, Int64, Matrix{Int64}, Vector{RGB{Float64}}}:
...

julia> save("indexed_image.png", img)

Теперь откройте новый сеанс REPL Julia (это необходимо для демонстрации проблемы) и вызовите load из функции (это также важно):

julia> using FileIO

julia> f() = size(load("indexed_image.png"))
f (generic function with 1 method)

julia> f()
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}}}})
The applicable method may be too new: running in world age 32382, while current world is 32416.
Closest candidates are:
  size(::IndirectArrays.IndirectArray) at ~/.julia/packages/IndirectArrays/BUQO3/src/IndirectArrays.jl:52 (method too new to be called from this world context.)
  size(::AbstractArray{T, N}, ::Any) where {T, N} at abstractarray.jl:42
  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
  ...
Stacktrace:
 [1] f()
   @ Main ./REPL[2]:1
 [2] top-level scope
   @ REPL[3]:1

Чтобы понять, почему это произошло, необходимо разобраться в очередности событий:

  1. При вызове f() из REPL среда Julia сначала скомпилировала f. Важно отметить, что при компиляции среде Julia было неизвестно, объект какого типа вернет load, поэтому в скомпилированном коде она ожидает возврата объекта, прежде чем решать, какой метод size следует вызвать. (Это называется диспетчеризацией во время выполнения.)

  2. Среда запросила файл, распознала его как PNG и загрузила пакеты ImageIO и PNGFiles. (Именно для загрузки этих пакетов необходимо было запустить новый сеанс Julia.)

  3. FileIO вызывает функцию load, предназначенную для формата PNG, из PNGFiles. (Об этом шаге мы поговорим более подробно ниже.) В результате возвращается изображение, которое представляет собой массив типа, определенного в пакете IndirectArrays (зависимость пакета PNGFiles).

  4. f вызывает size для возвращенного изображения. Однако этот вызов завершается сбоем, так как в момент вызова f пакет IndirectArrays не был загружен.

Иными словами, метод size для IndirectArray находится в более новом «мире», чем тот, из которого была вызвана функция f(). Это приводит к ошибке, которую мы наблюдали.

«Возраст мира» крайне важен для того, чтобы в Julia можно было переопределять методы в интерактивном режиме, но неприятным побочным эффектом является описываемая ошибка.

Радует то, что ее легко исправить, просто вызвав f() еще раз:

julia> f()
(4, 4)

Второй вызов f() срабатывает правильно, потому что на этот раз вы вызываете f() в новейшем «возрасте мира», где размер size(::IndirectArray) уже определен. По сути, с вводом каждого оператора в REPL вы переходите к новейшему «возрасту мира».

Решения

Base.invokelatest

Одним из решений является вызов size посредством функции Base.invokelatest, которая предназначена специально для обхода этой проблемы с диспетчеризацией «возраста мира». А именно — invokelatest диспетчеризует предоставленный вызов с использованием новейшего «возраста мира» (который может быть более поздним, чем действовавший на момент ввода f() в REPL). В новом сеансе Julia:

julia> using FileIO

julia> f() = Base.invokelatest(size, load("indexed_image.png"))
f (generic function with 1 method)

julia> f()
(4, 4)

На шаге 3 выше («FileIO вызывает функцию load, предназначенную для формата PNG, из PNGFiles») вызов функции load, определенной в PNGFiles, осуществляется посредством invokelatest. В противном случае ошибки, связанные с «возрастом мира», происходили бы даже при обычном интерактивном использовании FileIO (без помещения load внутрь функции).

Функция invokelatest существенно замедляет работу кода. Используйте ее только при крайней необходимости.

Немедленная загрузка необходимых пакетов

Другое решение проблемы «возраста мира» очень простое и не имеет отдаленных последствий: загружайте необходимые пакеты сразу же. Например, если проблема «возраста мира» вызвана методами, связанными с IndirectArray, загрузите пакет IndirectArrays немедленно:

julia> using FileIO, IndirectArrays # попробуйте выполнить эти инструкции в новом сеансе REPL Julia

julia> f() = size(load("indexed_image.png"))
f (generic function with 1 method)

julia> f()
(4, 4)

Таким образом, если вы хотите выполнить сборку пакета, это может выглядеть так:

module MyFancyPackage

# Таким образом обеспечивается загрузка IndirectArrays сразу при загрузке `MyFancyPackage`,
# чтобы проблема «возраста мира» не возникала.
using IndirectArrays, FileIO

f(file) = length(load(file))
end

Применяйте отложенную загрузку при работе с FileIO, но помните, что ускорение загрузки имеет подводные камни.