Вывод
Принцип работы вывода
Вывод типов — это процесс вывода типов последующих значений из типов входных значений. Подход Julia к выводу был описан в публикациях блога (1, 2).
Отладка compiler.jl
Вы можете начать сеанс Julia, отредактировать файл compiler/*.jl
(например, чтобы вставить операторы print
), а затем заменить Core.Compiler
в выполняющемся сеансе, перейдя к base
и выполнив include("compiler/compiler.jl")
. Это способствует гораздо быстрой разработке, чем если бы вы перестраивали Julia для каждого изменения.
Или можно использовать пакет Revise.jl для отслеживания изменений компилятора, используя команду Revise.track(Core.Compiler)
в начале сеанса Julia. Как объясняется в документации по Revise, изменения в компиляторе будут отражены после сохранения измененных файлов.
Удобной точкой входа в вывод типов является typeinf_code
. Вот пример, демонстрирующий выполнение вывода в convert(Int, UInt(1))
.
# Получаем метод
atypes = Tuple{Type{Int}, UInt} # типы аргументов
mths = methods(convert, atypes) # стоит проверить, что существует только один
m = first(mths)
# Создаем переменные, необходимые для вызова `typeinf_code`
interp = Core.Compiler.NativeInterpreter()
sparams = Core.svec() # у этого конкретного метода нет параметров типа
optimize = true # выполнить все оптимизации вывода
types = Tuple{typeof(convert), atypes.parameters...} # Tuple{typeof(convert), Type{Int}, UInt}
Core.Compiler.typeinf_code(interp, m, types, sparams, optimize)
Если для отладки требуется экземпляр MethodInstance
, его можно найти, вызвав метод Core.Compiler.specialize_method
и используя многие из вышеперечисленных переменных. Объект CodeInfo
можно получить следующим образом:
# Возвращает объект CodeInfo для `convert(Int, ::UInt)`:
ci = (@code_typed convert(Int, UInt(1)))[1]
Алгоритм встраивания (inline_worthy
)
Большая часть самой сложной работы по встраиванию выполняется в ssa_inlining_pass!
. Однако если вы захотите узнать, почему функция не встраивается, вас, скорее всего, заинтересует алгоритм inline_worthy
, который принимает решение о встраивании вызова функции.
inline_worthy
реализует модель стоимости, где «дешевые» функции встраиваются. В частности, мы встраиваем функции, если их предполагаемое время выполнения невелико по сравнению со временем, которое потребуется для их вызова, если они не будут встроены. Модель стоимости чрезвычайно проста и игнорирует многие важные детали: например, все циклы for
анализируются так, как будто они будут выполнены один раз, а в стоимость if...else...end
входит суммарная стоимость всех ветвей. Стоит также признать, что в настоящее время у нас нет набора функций, подходящих для проверки того, насколько хорошо модель затрат предсказывает фактические затраты во время выполнения, хотя BaseBenchmarks предоставляет большой объем косвенной информации об успешных и неудачных изменениях алгоритма встраивания.
Основой модели затрат является таблица поиска, реализованная в функции add_tfunc
и ее вызывающих объектах, которая присваивает оценочную стоимость (измеряемую в циклах ЦП) каждой внутренней функции Julia. Эти затраты основаны на стандартных диапазонах для распространенных архитектур (более подробное описание см. в анализе Агнера Фога (Agner Fog)).
Мы дополняем эту низкоуровневую таблицу поиска рядом специальных случаев. Например, выражению :invoke
(вызову, для которого все входные и выходные типы были определены заранее) присваивается фиксированная стоимость (в настоящее время — 20 циклов). Напротив, выражение :call
для функций, отличных от внутренних или встроенных, указывает на то, что для вызова потребуется динамическая диспетчеризация, и в этом случае мы присваиваем стоимость, заданную с помощью Params.inline_nonleaf_penalty
(в настоящее время установлено значение 1000
). Обратите внимание, что это не «первопринципная» оценка исходной стоимости динамической диспетчеризации, а просто эвристика, указывающая на чрезвычайную дороговизну динамической диспетчеризации.
Каждый оператор анализируется на предмет его общей стоимости в функции, называемой statement_cost
. Стоимость, связанную с каждым оператором, можно отобразить следующим образом:
julia> Base.print_statement_costs(stdout, map, (typeof(sqrt), Tuple{Int},)) # map(sqrt, (2,))
map(f, t::Tuple{Any}) @ Base tuple.jl:273
0 1 ─ %1 = Base.getfield(_3, 1, true)::Int64
1 │ %2 = Base.sitofp(Float64, %1)::Float64
2 │ %3 = Base.lt_float(%2, 0.0)::Bool
0 └── goto #3 if not %3
0 2 ─ invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %2::Float64)::Union{}
0 └── unreachable
20 3 ─ %7 = Base.Math.sqrt_llvm(%2)::Float64
0 └── goto #4
0 4 ─ goto #5
0 5 ─ %10 = Core.tuple(%7)::Tuple{Float64}
0 └── return %10
Стоимость строки указана в левом столбце. Сюда входят последствия встраивания и других форм оптимизации.