Does calling a function with no memory allocations create an allocation?

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.