Output
The principle of operation of the output
In the Julia compiler, type inference is the process of inferring the types of subsequent values from the types of input values. Julia’s approach to inference has been described in the blog posts below.
-
https://info.juliahub.com/inference-convergence-algorithm-in-julia-revisited [The refinement of the algorithm presented in 2 is explained.]
Debugging compiler.jl
You can start a Julia session, edit the compiler/*.jl
file (for example, to insert print
statements), and then replace Core.Compiler
in the running session by going to base
and executing include("compiler/compiler.jl")
. This contributes to much faster development than if you were rebuilding Julia for every change.
Or you can use a package https://github.com/timholy/Revise .jl[Revise.jl] to track compiler changes using the Revise.track(Core.Compiler)
at the beginning of the Julia session. As explained in https://timholy .github.io/Revise.jl/stable/[Revise documentation], changes in the compiler will be reflected after saving the modified files.
A convenient entry point to type inference is `typeinf_code'. Here is an example demonstrating the execution of the output in `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() # у этого конкретного метода нет параметров типа
run_optimizer = true # выполнить все оптимизации вывода
types = Tuple{typeof(convert), atypes.parameters...} # Tuple{typeof(convert), Type{Int}, UInt}
Core.Compiler.typeinf_code(interp, m, types, sparams, run_optimizer)
If an instance of MethodInstance
is required for debugging, it can be found by calling the 'Core' method.Compiler.specialize_method` and using many of the above variables. The 'CodeInfo` object can be obtained as follows:
# Возвращает объект CodeInfo для `convert(Int, ::UInt)`:
ci = (@code_typed convert(Int, UInt(1)))[1]
Embedding algorithm (`inline_worthy')
Most of the most difficult embedding work is done in ssa_inlining_pass!
. However, if you want to know why the function is not embedded, you will most likely be interested in the inline_worthy
algorithm, which decides whether to embed a function call.
inline_worthy
implements a cost model where "cheap" features are embedded. In particular, we embed functions if their estimated execution time is small compared to the time it will take to execute them. https://en.wikipedia.org/wiki/Calling_convention [call] if they are not embedded. The cost model is extremely simple and ignores many important details: for example, all for
loops are analyzed as if they will be executed once, and the cost of if...else...end
includes the total cost of all branches. It’s also worth admitting that we currently don’t have a set of features suitable for testing how well the cost model predicts the actual costs at runtime, though https://github.com/JuliaCI/BaseBenchmarks.jl [BaseBenchmarks] provides a large amount of indirect information about successful and unsuccessful changes to the embedding algorithm.
The basis of the cost model is a lookup table implemented in the add_tfunc
function and its callers, which assigns an estimated cost (measured in CPU cycles) to each internal Julia function. These costs are based on http://ithare.com/wp-content/uploads/part101_infographics_v08.png [standard ranges for common architectures] (for a more detailed description, see https://www.agner.org/optimize/instruction_tables.pdf [analysis of Agner Fog]).
We supplement this low-level lookup table with a number of special cases. For example, the expression :invoke
(a call for which all input and output types have been defined in advance) is assigned a fixed cost (currently 20 cycles). On the contrary, the expression :call
for functions other than internal or built-in indicates that dynamic dispatching will be required for the call, in which case we assign the value set using Params.inline_nonleaf_penalty
(currently set to `1000'). Please note that this is not a "first-principle" estimate of the initial cost of dynamic dispatching, but simply a heuristic indicating the extreme high cost of dynamic dispatching.
Each statement is analyzed for its total value in a function called `statement_cost'. The cost associated with each operator can be displayed as follows:
julia> Base.print_statement_costs(stdout, map, (typeof(sqrt), Tuple{Int},)) # map(sqrt, (2,))
map(f, t::Tuple{Any}) @ Base tuple.jl:281
0 1 ─ %1 = $(Expr(:boundscheck, true))::Bool
0 │ %2 = Base.getfield(_3, 1, %1)::Int64
1 │ %3 = Base.sitofp(Float64, %2)::Float64
0 │ %4 = Base.lt_float(%3, 0.0)::Bool
0 └── goto #3, если не %4
0 2 ─ invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %3::Float64)::Union{}
0 └── unreachable
20 3 ─ %8 = Base.Math.sqrt_llvm(%3)::Float64
0 └── goto #4
0 4 ─ goto #5
0 5 ─ %11 = Core.tuple(%8)::Tuple{Float64}
0 └── return %11
The cost of the row is indicated in the left column. This includes the consequences of embedding and other forms of optimization.