Varargs performance

I’m experiencing some poor performance when using varargs. I can work around it by writing more code, but would really like to understand what the specific issue is.

Here is an attempt at a minimal working example, indicating a severe penalty from passing arguments for a function to be called with using varargs

You may notice that the example is a bit strange, but changing many things in isolation eliminates the allocations, e.g. if

  1. g returns rand() < 0.5,
  2. g returns x < y, 1
using BenchmarkTools

function foo(f::F, m::Int64, args...) where F<:Function
  for i in 1:m
    z = f(args...)
  end
  return true
end

function g(x::Float64, y::Float64)
  return rand() < 0.5, 1
end

function bar(f::F, m::Int64, x::Float64, y::Float64) where F<:Function
  for i in 1:m
    z = f(x, y)
  end
  return true
end

@btime foo(g, 1000, 1.0, 2.0) | 39.059 μs (1000 allocations: 31.25 KiB)
@btime bar(g, 1000, 1.0, 2.0) | 1.260 μs (0 allocations: 0 bytes)

The only obvious difference in @code_lowered for the two functions on these arguments is

z = (Core._apply)(f, args) for foo and z = (f)(x, y) for bar.

1 Like

Interesting. I can reproduce this on v0.6.4, v0.7, and v1.0. Might be worth opening an issue over at https://github.com/julialang/julia if no one else here has any other suggestions.

1 Like

In case anyone in the future is interested, there is some discussion of this at https://github.com/JuliaLang/julia/issues/28720.

To summarize a bit of it, for examples like this one can eliminate allocations by using

function foo(f::F, m::Int64, args::Vararg{Any,N}) where {F<:Function, N}

as suggested by @pablosanjose. In other cases, this might not be sufficient, in particular (I suspect) when one should really include more type information about the arguments to get good performance; but this doesn’t really have anything to do with varargs per se.

Perhaps I’m being dense, but how does ::Vararg{Any,N} give the compiler any more information than it already had. Isn’t ::Vararg{Any,N} as generic as possible?

Yes, that’s part of the reason for the issue @alee linked to above. As far as I understand, args... should be equivalent to Vararg{Any,N} where N. I think there is a legitimate issue here.

Same as with function arguments there are likely some heuristics to not specialize. Adding parameterization forces specialization.