Hi all,
I am running a numerical simulation in which I document my results in a pre-allocated structure, Obs:
mutable struct Obs{T}
name::String
data::Array{T}
num_of_measure::Int64
c_num_of_measure::Int64
end
For this example, I will measure scalars:
measurements = Obs{Float64}("scalar",fill(0.0,1000),1000,0);
by updating this structure with a function:
function take_measurement!(obs_data::Obs{Float64},data::Float64)
obs_data.c_num_of_measure+=1
obs_data.data[obs_data.c_num_of_measure]=data
end
My question is why does calling this function allocates memory, and whether or not it is optimal. I get (after compiling the function):
@time for i =1:1000
take_measurement!(measurements,1.0)
end
0.000060 seconds (1.49 k allocations: 23.266 KiB)
Can anyone explain where exactly is this memory coming from? Is there a better way of measuring my data?
To anyone reading the solution: note that this structure is unnecessary as noted in the comments
Thanks,
Omer
1 Like
Array{T}
is a non-concrete type. Thus, when the code fetches this value from the struct is cannot be certain how many dimensions it has. This results in dynamic dispatch. The allocations should disappear if you use Vector{T}
(or equivalently: Array{T,1}
).
1 Like
Also, be careful benchmarking in global scope. It’s safest to use e.g. BenchmarkTools.jl and do
@btime take_measurement!($measurements,1.0)
where @btime
will automatically run the benchmark multiple times to get good timing resolution, and the $
will eliminate any cost from using a global variable measurements
. (You could also use the @b
macro from the Chairmarks.jl package.)
2 Likes
By the way, it looks like you’re unnecessarily recreating the machinery of Vector
here. It has the ability to over-allocate and then have entries added efficiently over time. For example:
struct Obs{T}
name::String
data::Vector{T}
end
measurements = Obs{Float64}("scalar",empty!(Vector{Float64}(undef,1000))); # allocate space for 1000 elements but then drop them
function take_measurement!(obs_data::Obs, data)
push!(obs_data.data, data)
end
clear!(obs::Obs) = empty!(obs.data)
clear!(measurements)
@time for i =1:1000 # run once to compile
take_measurement!(measurements,1.0)
end
clear!(measurements)
@time for i =1:1000 # run again for speed
take_measurement!(measurements,1.0)
end
gives me
0.003733 seconds (2.02 k allocations: 140.328 KiB, 98.84% compilation time)
0.000026 seconds
Note that, with this version, you aren’t limited to just the 1000 measurements you initially planned for. It can add new measurements until you run out of memory. When you clear!
it, it will usually keep that memory around so that you don’t need to reallocate it if you start a new set later.
Also, with those extra fields gone it looks like you can use a struct
rather than mutable struct
. I usually find struct
easier, safer, and faster to work with.
Do note that, in this case, this will not reset the vector between runs and its memory use will grow over the benchmark. With the original implementation, the benchmark crashes when the vector exceeds the allocated 1000 elements. Benchmarking with mutable objects is a little more tedious than usual. (obviously, stevengj
knows this and just overlooked it this time – I say this for others’ benefit).
My revised version that expands the vector via push!
does not suffer this limitation. But still, if one prefers to avoid measuring the cost of increasingly large re-allocations, one can clear!
it periodically. Like this:
julia> using BenchmarkTools
julia> @btime take_measurement!($measurements,1.0) setup=(clear!(measurements))
6.807 ns (0 allocations: 0 bytes)
1 Like
Thank you, I did not know this possibility was built in Vector.