Rahotop
Rahotop

Reputation: 45

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.

Upvotes: 3

Views: 484

Answers (2)

hckr
hckr

Reputation: 5583

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.

Upvotes: 4

Przemyslaw Szufel
Przemyslaw Szufel

Reputation: 42264

  1. Never use abstract types in struct definitions. In your example Julia needs to store variable pointers instead of just values and hence the speed degradation. Instead use parametric types:
julia> struct X{T}
           mat::T
           end

julia> X{Float64}.(1:3)
5-element Array{X{Float64},1}:
 X{Float64}(1.0)
 X{Float64}(2.0)
 X{Float64}(3.0)
  1. If unsure, consider using @code_warntype macro to see where the Julia compiler can not properly identify types.

Upvotes: 4

Related Questions