I have recently started using Julia to speed up some code previously written in Python. I only have prior experience with Python, so this is my first time caring about performance and I have found some strange behavior when looping over an array of structs. I am defining a new struct Gaussian, which represent a 2d Gaussian function and a function intensity() which calculates the amplitude of the function at a given position:
struct Gaussian{T<:Float32}
x0::T
y0::T
A::T
a::T
b::T
c::T
end
function intensity(
model::Gaussian,
x::Float32,
y::Float32
)
gaussian_value::Float32 = model.A*exp(
-(
model.a * (x - model.x0)^2 +
2 * model.b * (x - model.x0) * (y - model.y0) +
model.c * (y - model.y0)^2
)
)
return gaussian_value
end
Then, I make an array of 2000 random instances of Gaussian:
function build_array()
length = 2000
random_pos = [rand(Float32, (1, 2)) for i in 1:length]
random_A = rand(Float32, (length, 1))
random_a = rand(Float32, (length, 1))
random_b = rand(Float32, (length, 1))
random_c = rand(Float32, (length, 1));
gaussians::Array{Gaussian} = []
for (pos, A, a, b, c) in zip(
random_pos,
random_A,
random_a,
random_b,
random_c
)
new_gaussian = Gaussian(pos..., A, a, b, c)
push!(gaussians, new_gaussian)
end
return gaussians
end
gaussians = build_array()
When I benchmark a single call to the intensity() function, it takes ~100 ns with 1 allocation (makes sense). I would expect that looping over the array of Gaussians should then take 2000*100 ns = 200 us. However, it actually takes about twice as long:
function total_intensity1(gaussian_list::Array{Gaussian})
total = sum(intensity.(gaussian_list, Float32(0.75), Float32(0.11)))
end
function total_intensity2(gaussian_list::Array{Gaussian})
total::Float32 = 0.
for gaussian in gaussian_list
total += intensity(gaussian, Float32(0.75), Float32(0.11))
end
return total
end
@btime sum(intensity.(gaussians, Float32(0.75), Float32(0.11)))
@btime begin
total::Float32 = 0.
for gauss in gaussians
total += intensity(gauss, Float32(0.75), Float32(0.11))
end
total
end
@btime total_intensity1(gaussians)
@btime total_intensity2(gaussians)
397.700 μs (16004 allocations: 258.02 KiB)
285.800 μs (8980 allocations: 234.06 KiB)
396.100 μs (16002 allocations: 257.95 KiB)
396.700 μs (16001 allocations: 250.02 KiB)
The number of allocations is also much larger than I would expect and there is a difference between the second and fourth method even though the code is pretty much the same. My questions:
- Where do these differences come from?
- How can I improve the performance of the code?
EDIT: For reference, I ended up changing my code to the following:
struct Gaussian
x0::Float32
y0::Float32
A::Float32
a::Float32
b::Float32
c::Float32
end
function build_array()
N = 2000
random_pos = [rand(Float32, (1, 2)) for i in 1:N]
random_A = rand(Float32, N)
random_a = rand(Float32, N)
random_b = rand(Float32, N)
random_c = rand(Float32, N);
gaussians = Gaussian[]
for (pos, A, a, b, c) in zip(
random_pos,
random_A,
random_a,
random_b,
random_c
)
new_gaussian = Gaussian(pos..., A, a, b, c)
push!(gaussians, new_gaussian)
end
return gaussians
end
gaussians = build_array()
function intensity(
model::Gaussian,
x,
y
)
(;x0, y0, A, a, b, c) = model
A*exp(-(a * (x - x0)^2 + 2 * b * (x - x0) * (y - y0) + c * (y - y0)^2))
end
function total_intensity(gaussian_list::Vector{<:Gaussian})
total = sum(g->intensity(g, Float32(0.75), Float32(0.11)), gaussian_list)
end
@btime total_intensity($gaussians)
Which runs much faster:
10.900 μs (0 allocations: 0 bytes)
Thank you to Nils Gudat and DNF for their suggestions!