Проблема «возраста мира»
Назначение: отложенная загрузка
Целью пакета 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
Чтобы понять, почему это произошло, необходимо разобраться в очередности событий:
-
При вызове
f()
из REPL среда Julia сначала скомпилировалаf
. Важно отметить, что при компиляции среде Julia было неизвестно, объект какого типа вернетload
, поэтому в скомпилированном коде она ожидает возврата объекта, прежде чем решать, какой методsize
следует вызвать. (Это называется диспетчеризацией во время выполнения.) -
Среда запросила файл, распознала его как PNG и загрузила пакеты ImageIO и PNGFiles. (Именно для загрузки этих пакетов необходимо было запустить новый сеанс Julia.)
-
FileIO вызывает функцию
load
, предназначенную для формата PNG, из PNGFiles. (Об этом шаге мы поговорим более подробно ниже.) В результате возвращается изображение, которое представляет собой массив типа, определенного в пакете IndirectArrays (зависимость пакета PNGFiles). -
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 вызывает функцию |
Функция |
Немедленная загрузка необходимых пакетов
Другое решение проблемы «возраста мира» очень простое и не имеет отдаленных последствий: загружайте необходимые пакеты сразу же. Например, если проблема «возраста мира» вызвана методами, связанными с 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, но помните, что ускорение загрузки имеет подводные камни.