Search code examples
arraysstructjuliaallocation

Julia - many allocation to browse an array in struct


I'm currently struggling with a weird behaviour of Julia. I'm browsing through an array, and whether the array is inside a struct or not Julia doesn't behave the same.

There is many allocations that seem pointless in the case of an array inside a struct. To be specific there is as many more allocations as the size of the array.

Here's a code to replicate this problem :

function test1()
    a = ones(Float32, 256)

    for i = 1:256
        a[i]
    end
end

struct X
    mat
end

function test2()
    a = X(ones(Float32, 256))

    for i = 1:256
        a.mat[i]
    end
end

function main()
    test1()
    test2()

    @time test1()
    @time test2()
end

main()

And the output I get :

0.000002 seconds (1 allocation: 1.141 KiB)
0.000012 seconds (257 allocations: 5.141 KiB)

At first I thought it was a type problem, but I don't force it and the type isn't different after the loop.

Thanks for your help.


Solution

  • You need to specify the type of mat in your struct. Otherwise, your functions using X will not specialize and be optimized enough.

    Fields with no type annotation default to Any, and can accordingly hold any type of value. https://docs.julialang.org/en/v1/manual/types/index.html#Composite-Types-1

    Changing your struct definition to

    struct X
        mat::Vector{Float32}
    end
    

    will solve the problem. The results now are:

      0.000000 seconds (1 allocation: 1.141 KiB)
      0.000000 seconds (1 allocation: 1.141 KiB)
    

    You could actually see the effect through @code_warntype macro if you change one thing in your code.

    for i = 1:256
        a.mat[i]
    end
    

    This part is not really doing much. To see the effect with @code_warntype change this line in your old code to

    for i = 1:256
        a.mat[i] += 1.
    end
    

    The result of @code_warntype will give Any in red color which you should usually avoid. The reason is the type of mat is not known at compile time.

    > @code_warntype test2() # your test2() with old X def
    Body::Nothing
    1 ─ %1  = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Float32,1}, svec(Any, Int64), :(:ccall), 2, Array{Float32,1}, 256, 256))::Array{Float32,1}
    │   %2  = invoke Base.fill!(%1::Array{Float32,1}, 1.0f0::Float32)::Array{Float32,1}
    └──       goto #7 if not true
    2 ┄ %4  = φ (#1 => 1, #6 => %14)::Int64
    │   %5  = φ (#1 => 1, #6 => %15)::Int64
    │   %6  = (Base.getindex)(%2, %4)::Any <------ See here
    │   %7  = (%6 + 1.0)::Any
    │         (Base.setindex!)(%2, %7, %4)
    │   %9  = (%5 === 256)::Bool
    └──       goto #4 if not %9
    3 ─       goto #5
    4 ─ %12 = (Base.add_int)(%5, 1)::Int64
    └──       goto #5
    5 ┄ %14 = φ (#4 => %12)::Int64
    │   %15 = φ (#4 => %12)::Int64
    │   %16 = φ (#3 => true, #4 => false)::Bool
    │   %17 = (Base.not_int)(%16)::Bool
    └──       goto #7 if not %17
    6 ─       goto #2
    7 ┄       return
    

    Now with the new definition of X, you will see in th result of @code_warntype every type is inferred.

    You may want to use Parametric Types if you want X.mat to hold other types of Vectors or values. With parametric types, the compiler will still be able to optimize your functions since the type will be known during compilation. I would really recommend you to read the relevant manual entry for types and performance tips.