Type Stability
Julia is fast because the compiler generates specialized machine code for the argument types of each call. That only works well if it can infer the type of every variable and the return value from the argument types alone. Code where it can is type-stable.
unstable(x) = x > 0 ? x : 0.0 # Int or Float64, depending on the value
stable(x) = x > 0 ? x : zero(x) # always the same type as x
println(Base.return_types(unstable, (Int,)))
println(Base.return_types(stable, (Int,)))
println(typeof(unstable(5)), " ", typeof(unstable(-5)))
Any[Union{Float64, Int64}]
Any[Int64]
Int64 Float64
unstable returns an Int for positive input and a Float64
otherwise, so its inferred type is a Union. Every caller then has to
check which one it got. zero(x) returns a zero of the same type as
x — the fix.
@code_warntype
In the REPL, @code_warntype f(args...) shows what the compiler
inferred. Problems are highlighted in red and the return type appears
after Body:
julia> @code_warntype unstable(1)
MethodInstance for unstable(::Int64)
from unstable(x) @ Main REPL[1]:1
Arguments
#self#::Core.Const(Main.unstable)
x::Int64
Body::UNION{FLOAT64, INT64}
...
Look for Any, Union, or red text in the variables and the body.
The JET.jl package (@report_opt) finds these problems across a
whole call tree, and Cthulhu.jl lets you descend into nested calls.
Testing for stability
using Test
stable(x) = x > 0 ? x : zero(x)
unstable(x) = x > 0 ? x : 0.0
println(@inferred stable(3))
try
@inferred unstable(3)
catch err
println(sprint(showerror, err))
end
3
return type Int64 does not match inferred return type Union{Float64, Int64}
@inferred throws if the actual return type differs from the inferred
one — use it in your test suite for performance-critical functions.
Common causes of instability
# 1. A variable that changes type inside a loop
function sum_unstable(xs)
total = 0 # Int...
for x in xs
total += x # ...becomes Float64 for float input
end
return total
end
function sum_stable(xs)
total = zero(eltype(xs))
for x in xs
total += x
end
return total
end
println(Base.return_types(sum_unstable, (Vector{Float64},)))
println(Base.return_types(sum_stable, (Vector{Float64},)))
# 2. Abstract field types
struct Loose; v::Real; end
struct Tight{T<:Real}; v::T; end
getv(s) = s.v
println(Base.return_types(getv, (Loose,)), " ", Base.return_types(getv, (Tight{Float64},)))
# 3. Containers with abstract element types
println(eltype(Real[1, 2.0]), " ", eltype([1, 2.0]))
Any[Union{Float64, Int64}]
Any[Float64]
Any[Real] Any[Float64]
Real Float64
- Initialize accumulators with
zero(T)orzero(eltype(xs)). - Give struct fields concrete or parametric types.
- Prefer
Vector{Float64}overVector{Real}orVector{Any}. - Untyped global variables are the fourth big cause — see the lesson on memory and globals.
Function barriers
When a value's type genuinely can't be known (for example, data read from a file), pass it to a separate function. Julia compiles that inner function for the concrete type it actually receives:
function process(data)
values = data["values"] # the compiler sees ::Any here
return kernel(values) # ...but kernel is specialized
end
function kernel(v)
s = zero(eltype(v))
for x in v
s += x * x
end
return s
end
data = Dict{String,Any}("values" => [1.0, 2.0, 3.0], "name" => "demo")
println(process(data))
14.0