On programming with Julia

工作集

On programming with Julia

Julia is my favorite programming language for scientific computing. As a dynamically typed language, it compiles efficient native code based on LLVM. This leads to the popular phrase in the Julia community, “walk like Python, run Like C.” The multiple dispatch as a new paradigm for language design makes it natural to write performant and extendable codes.

Although Julia was designed from the beginning for numerical computing, it is unlikely for a beginner to write high-performance codes right out of the box like Fortran and C. In this post, I would like to share some common tips that help you to write performant codes.

Parameterized structure

Julia’s functions are generic methods. Thanks to the type inference, cumbersome type annotation is not needed for the arguments. For example, the following function

time2(x) = 2 * x

is as fast as its annotated version specifically designed for a 64 bit float number

time2_an(x::Float64) = 2 * x

However, the same statement does not hold for composite data types. Let us take a look at the following example.

using BenchmarkTools
struct A
    a
    b
    c
end

struct B{T1,T2,T3}
    a::T1
    b::T2
    c::T3
end
a = [A(rand(), rand(), rand()) for i = 1:10000];
b = [B(rand(), rand(), rand()) for i = 1:10000];
function add(x)
    for i in eachindex(x)
        x[i].a + x[i].b + x[i].c
    end
end
add (generic function with 1 method)
@btime add(a)
  120.258 μs (10000 allocations: 156.25 KiB)
@btime add(b)
  2.331 μs (0 allocations: 0 bytes)
@code_warntype add(a)
Variables
  #self#::Core.Const(add)
  x::Vector{A}
  @_3::Union{Nothing, Tuple{Int64, Int64}}
  i::Int64

Body::Nothing
1 ─ %1  = Main.eachindex(x)::Base.OneTo{Int64}
│         (@_3 = Base.iterate(%1))
│   %3  = (@_3 === nothing)::Bool
│   %4  = Base.not_int(%3)::Bool
└──       goto #4 if not %4
2 ┄ %6  = @_3::Tuple{Int64, Int64}::Tuple{Int64, Int64}
│         (i = Core.getfield(%6, 1))
│   %8  = Core.getfield(%6, 2)::Int64
│   %9  = Base.getindex(x, i)::A
│   %10 = Base.getproperty(%9, :a)::Any
│   %11 = Base.getindex(x, i)::A
│   %12 = Base.getproperty(%11, :b)::Any
│   %13 = Base.getindex(x, i)::A
│   %14 = Base.getproperty(%13, :c)::Any
│         (%10 + %12 + %14)
│         (@_3 = Base.iterate(%1, %8))
│   %17 = (@_3 === nothing)::Bool
│   %18 = Base.not_int(%17)::Bool
└──       goto #4 if not %18
3 ─       goto #2
4 ┄       return nothing
@code_warntype add(b)
Variables
  #self#::Core.Const(add)
  x::Vector{B{Float64, Float64, Float64}}
  @_3::Union{Nothing, Tuple{Int64, Int64}}
  i::Int64

Body::Nothing
1 ─ %1  = Main.eachindex(x)::Base.OneTo{Int64}
│         (@_3 = Base.iterate(%1))
│   %3  = (@_3 === nothing)::Bool
│   %4  = Base.not_int(%3)::Bool
└──       goto #4 if not %4
2 ┄ %6  = @_3::Tuple{Int64, Int64}::Tuple{Int64, Int64}
│         (i = Core.getfield(%6, 1))
│   %8  = Core.getfield(%6, 2)::Int64
│   %9  = Base.getindex(x, i)::B{Float64, Float64, Float64}
│   %10 = Base.getproperty(%9, :a)::Float64
│   %11 = Base.getindex(x, i)::B{Float64, Float64, Float64}
│   %12 = Base.getproperty(%11, :b)::Float64
│   %13 = Base.getindex(x, i)::B{Float64, Float64, Float64}
│   %14 = Base.getproperty(%13, :c)::Float64
│         (%10 + %12 + %14)
│         (@_3 = Base.iterate(%1, %8))
│   %17 = (@_3 === nothing)::Bool
│   %18 = Base.not_int(%17)::Bool
└──       goto #4 if not %18
3 ─       goto #2
4 ┄       return nothing