Passing `Symbol` variable allocates when literal does not?

I have the following snippet:

function preparearrays!(arrays, N, L; mode = :all)
    for n in 0:(length(arrays)-1)
        if mode === :all
            for m in 0:(L-1) # run till length L to zero all bits
                arrays[n+1][m+1] = (n & (1 << m)) >> m
            end
        elseif mode === :basis
            arrays[n+1] .= 0;
            arrays[n+1][n+1] = 1;
        end
    end
    return arrays
end

preparearrays(N, L; mode = :all) = preparearrays!(collect(zeros(Int64, L) for i in 1:ifelse(mode==:basis, N, 2^N)), N, L; mode = mode)

My issue is that I want to set a variable at the start of my calculation, mode = :all or mode = :basis. (The above pattern appears in other places in my code.) However, calling the above function with a variable seems to allocate (and dramatically regress performance):

arr = preparearrays(8, 16; mode = :basis);
@btime preparearrays!(arr, 8, 16; mode = :basis);
# 62.245 ns (0 allocations: 0 bytes)
var = :basis;
@btime preparearrays!(arr, 8, 16; mode = var);
# 183.043 ns (2 allocations: 32 bytes)

Any idea where the allocation comes from, and how to avoid it?

This looks like a benchmarking artifact. Check the documentation of BencharkTools.jl: you should interpolate the global variables that you pass to the benchmarked code:

julia> @btime preparearrays!(arr, 8, 16; mode = var);
  171.130 ns (2 allocations: 32 bytes)

julia> @btime preparearrays!(arr, 8, 16; mode = $var);
  50.771 ns (1 allocation: 16 bytes)
1 Like

You are right. I had only tried interpolated $arr

@sijo though in my defense, @time and @allocated (which I don’t believe support interpolation?) do report these allocation.

For these macros I think to avoid these extra allocations you need to use typed globals (or constants but then the benchmark might be affected by constant propagation?)

using BenchmarkTools

function preparearrays!(arrays, N, L; mode = :all)
    for n in 0:(length(arrays)-1)
        if mode === :all
            for m in 0:(L-1) # run till length L to zero all bits
                arrays[n+1][m+1] = (n & (1 << m)) >> m
            end
        elseif mode === :basis
            arrays[n+1] .= 0;
            arrays[n+1][n+1] = 1;
        end
    end
    return arrays
end

preparearrays(N, L; mode = :all) = preparearrays!(collect(zeros(Int64, L) for i in 1:ifelse(mode==:basis, N, 2^N)), N, L; mode = mode)

arr::Vector{Vector{Int64}} = preparearrays(8, 16; mode = :basis);
var::Symbol = :basis

With this I get

julia> @allocated preparearrays!(arr, 8, 16; mode=var)
0

You can also make a wrapper function:

using BenchmarkTools

function preparearrays!(arrays, N, L; mode = :all)
    for n in 0:(length(arrays)-1)
        if mode === :all
            for m in 0:(L-1) # run till length L to zero all bits
                arrays[n+1][m+1] = (n & (1 << m)) >> m
            end
        elseif mode === :basis
            arrays[n+1] .= 0;
            arrays[n+1][n+1] = 1;
        end
    end
    return arrays
end

preparearrays(N, L; mode = :all) = preparearrays!(collect(zeros(Int64, L) for i in 1:ifelse(mode==:basis, N, 2^N)), N, L; mode = mode)

function get_allocated(arr, var)
    @allocated preparearrays!(arr, 8, 16; mode=var)
end

arr = preparearrays(8, 16; mode = :basis);
var = :basis;

get_allocated(arr, var)  # returns 0