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.
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 Vector
s 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.