Skip to content

Vararg vs "..." slurping has huge performance impacts which defy analysis #32761

@iamed2

Description

@iamed2

Here are two almost-identical functions:

function getindex1!(dest::AbstractArray, src::AbstractArray, I::Union{Real, AbstractArray}...)
    @inbounds for (i, j) in zip(eachindex(dest), Iterators.product(I...))
        dest[i] = src[j...]
    end
    return dest
end
function getindex2!(dest::AbstractArray, src::AbstractArray, I::Vararg{Union{Real, AbstractArray}, N}) where N
    @inbounds for (i, j) in zip(eachindex(dest), Iterators.product(I...))
        dest[i] = src[j...]
    end
    return dest
end

And now a benchmark:

julia> a = zeros(300, 300); b = rand(500, 500);

julia> @benchmark getindex1!($a, $b, 201:500, 201:500)
BenchmarkTools.Trial:
  memory estimate:  20.59 MiB
  allocs estimate:  629500
  --------------
  minimum time:     17.831 ms (0.00% GC)
  median time:      20.793 ms (0.00% GC)
  mean time:        20.941 ms (5.62% GC)
  maximum time:     31.831 ms (8.14% GC)
  --------------
  samples:          239
  evals/sample:     1

julia> @benchmark getindex2!($a, $b, 201:500, 201:500)
BenchmarkTools.Trial:
  memory estimate:  352 bytes
  allocs estimate:  6
  --------------
  minimum time:     77.827 μs (0.00% GC)
  median time:      77.973 μs (0.00% GC)
  mean time:        87.117 μs (0.00% GC)
  maximum time:     514.022 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1

The code_lowered and code_typed are identical.
For a function containing a call to this function, the code_typed is different:

julia> foo1(a, b) = getindex1!(a, b, 201:500, 201:500)
foo1 (generic function with 1 method)

julia> foo2(a, b) = getindex2!(a, b, 201:500, 201:500)
foo2 (generic function with 1 method)

julia> @code_typed foo1(a, b)
CodeInfo(
1 ─ %1 = invoke Main.getindex1!(_2::Array{Float64,2}, _3::Array{Float64,2}, $(QuoteNode(201:500))::UnitRange{Int64}, $(QuoteNode(201:500))::Vararg{UnitRange{Int64},N} where N)::Array{Float64,2}
└──      return %1
) => Array{Float64,2}

julia> @code_typed foo2(a, b)
CodeInfo(
1 ─ %1 = invoke Main.getindex2!(_2::Array{Float64,2}, _3::Array{Float64,2}, $(QuoteNode(201:500))::UnitRange{Int64}, $(QuoteNode(201:500))::UnitRange{Int64})::Array{Float64,2}
└──      return %1
) => Array{Float64,2}

The reason for this difference in performance/compilation is not clear. The function signatures describe identical methods. If you define both as methods for the same function, there is still only one method in the function. I think this is a bug?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions