Документация Engee

Вывод

Принцип работы вывода

Вывод типов — это процесс вывода типов последующих значений из типов входных значений. Подход 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

Стоимость строки указана в левом столбце. Сюда входят последствия встраивания и других форм оптимизации.