Performance Fundamentals

Julia is fast when code is written so the compiler can infer types.

Julia is fast when code is written so the compiler can infer types. The official *Performance Tips* boil down to a few rules.

23.1 Put performance-critical code in functions

Top-level code in global scope is slow; functions get compiled and specialized.

23.2 Avoid untyped global variables

Typed globals (1.8+): z::Int = 10.

x = 10                          # non-constant global: type can change → slow
f() = x + 1

const Y = 10                    # constant global: fast
g() = Y + 1

h(x) = x + 1                    # best: pass as an argument

23.3 Type stability

A function is type-stable if its return type depends only on argument types, not values.

Tools: @code_warntype, @code_typed, @code_llvm, @code_native, JET.jl (@report_opt f(x)), Cthulhu.jl (@descend).

unstable(x) = x > 0 ? x : 0          # returns Float64 or Int for Float64 input!
stable(x) = x > 0 ? x : zero(x)      # always typeof(x)

@code_warntype unstable(1.5)         # red `Union{Float64, Int64}` warnings
@code_warntype stable(1.5)

23.4 Avoid abstract field types in structs

Also avoid containers with abstract element types (Vector{Any}, Vector{Real}) in hot code.

struct Slow
    data::AbstractVector          # abstract field → boxing, dynamic dispatch
end
struct Fast{T<:AbstractVector}
    data::T                       # parametric → concrete when instantiated
end

23.5 Pre-allocate and work in place

function compute!(out, x)
    @inbounds for i in eachindex(out, x)
        out[i] = 2x[i]^2 + 1
    end
    return out
end
x = rand(10^6); out = similar(x)
compute!(out, x)                 # zero allocations
out .= 2 .* x .^ 2 .+ 1          # fused broadcast, in place

23.6 Use views for slices; loop in column-major order

function colsum(A)
    s = zero(eltype(A))
    for j in axes(A, 2), i in axes(A, 1)   # inner loop over rows = contiguous memory
        s += A[i, j]
    end
    s
end
@views sum(A[:, 1:10])

23.7 Function barriers

function process(data)                 # data has an unknown (mixed) type
    x = data["values"]                 # type unknown here
    return kernel(x)                   # kernel is compiled for the concrete type of x
end
kernel(x::Vector{Float64}) = sum(abs2, x)

23.8 Other tips

Don't change a variable's type inside a function.

Annotate keyword arguments when they influence return types, or pass types as ::Type{T} arguments.

Closures capturing reassigned variables get boxed (Core.Box) — use let blocks or avoid reassignment.

Avoid string concatenation in loops (use IOBuffer or join).

Use StaticArrays for small fixed-size vectors/matrices (2–100 elements).

Use @inbounds, @simd, @fastmath judiciously (after validating correctness).

Prefer Int/Float64 over BigInt/BigFloat unless needed.

Watch out for type piracy and invalidations that cause recompilation.