What does allocation mean when creating a new array


#1

Consider this basic function

function allocation_test()
    A = Vector{Float64}(undef, 15)
    return A
end

I have a few questions about the memory usage of this function.

julia> @time allocation_test();
  0.000003 seconds (5 allocations: 368 bytes)

julia> @btime allocation_test();
  34.844 ns (1 allocation: 208 bytes)
  1. Why the apparent difference in @time and @btime macros?
  2. What is the 368 bytes (and 208 bytes) measuring? I’ve created an array of 15 Float64 elements all taking 8 bytes at most. So shouldn’t the allocation be 8*15 = 120 bytes? Even if one accounts for “overhead” the 368 bytes is more than twice that number.
  3. Is this what it means by heap allocated ? If this was stack allocated, it would show as “0 allocations” right?
  4. If I now assign the function to a variable like:
julia> @time b=allocation_test();
  0.000005 seconds (6 allocations: 416 bytes)

why is there extra memory usage? Should b just point to the array already created on the heap?

Thanks,


#2
  1. Put things into functions if you want accurate allocation measurements (or like you saw, use BenchmarkTols):
julia> function allocation_test()
           A = Vector{Float64}(undef, 15)
           return A
       end
allocation_test (generic function with 1 method)

julia> f() = @time allocation_test()
f (generic function with 1 method)

julia> f()
  0.000001 seconds (1 allocation: 208 bytes)
  1. There is some overhead in allocating a julia array over just the content (https://github.com/JuliaLang/julia/blob/2e91c5e73af4e44c45e942e057e5c09fa062cf8f/src/julia.h#L166-L185)

  2. Yes.

  3. Because you are using more global variables, see 1.


#3

I don’t understand the point 1. I am calling a function. Are you saying that even if @time is run from a global scope, there are issues? So the allocations I saw were allocations @time was using for its own purpose?

I am having a hard time profiling my code because I don’t know how many “functions” to nest before calling @time.


#4

Global variables typically cause allocations. The @time macro expands to many variables, so if @time is called in global scope there will be a few extra allocations coming from the macro itself.

julia> @macroexpand @time a = 1+1
quote
    local #9#stats = (Base.gc_num)()
    local #11#elapsedtime = (Base.time_ns)()
    local #10#val = (a = 1 + 1)
    #11#elapsedtime = (Base.time_ns)() - #11#elapsedtime
    local #12#diff = (Base.GC_Diff)((Base.gc_num)(), #9#stats)
    (Base.time_print)(#11#elapsedtime, (#12#diff).allocd, (#12#diff).total_time, (Base.gc_alloc_count)(#12#diff))
    (Base.println)()
    #10#val
end

This is described in the performance tips section in the manual (https://docs.julialang.org/en/v1/manual/performance-tips/index.html#Measure-performance-with-@time-and-pay-attention-to-memory-allocation-1):

The 5 allocations seen are from running the @time macro itself in global scope. If we instead run the timing in a function, we can see that indeed no allocations are performed:

Either use BenchmarkTools or make sure @time is not called from global scope. The overhead will be in the hundreds of bytes so for most measurements it doesn’t matter.