Tuple or immutable struct as function argument regarding speed and optimization

I have a function with many named arguments that should not depend on global variables for performance reasons

In the following I will call the variables a,b,c, ... ,y,z

I want to replace them with an immutable struct or a a named tuple as one function argument:

Named Tuple

(a=<value>, b=<value>, c=<value>, …, y=<value>, z=<value>)



which has the disadvantage that I still need to memorize argument order when initializing with the constructor

Are there suggestions or patterns or caveats how to handle this problem?

As far as I know keyword arguments are not optimized by compiler so I want to avoid that restriction. Am I wrong with that?

I find threads like this: Performance of functions with keyword arguments
but also this younger one Performance of typed keyword arguments that says they have the same speed

you could use keyword arguments or this way so the order does not matter

julia> const MyNT = NamedTuple{(:a, :b, :c), T} where {T<:Tuple}
NamedTuple{(:a, :b, :c),T} where T<:Tuple

julia> MyNT( (b=3, a=1, c=2) )
(a = 1, b = 3, c = 2)

it also works with different types for a,b,c.

Keyword arguments are not optimized by compiler

how do you feel about the NamedTuples approach?

Yeah that’s what I also would try. Just want to hear Pros and Cons

here’s one of each :slight_smile:
namedtuple Pro: it does what you want
struct Con: it does not do what you want

This is no longer true. Keyword arguments are implemented using named tuples since Julia 0.7/1.0 and are now much much faster.

For example:

julia> f1(;x = 1, y = 2) = x + y
f1 (generic function with 1 method)

julia> f2(xy::NamedTuple) = xy.x + xy.y
f2 (generic function with 1 method)

julia> struct XY{T}

julia> f3(xy::XY) = xy.x + xy.y
f3 (generic function with 1 method)

julia> using BenchmarkTools

julia> @btime f1(x = 1, y = 2)
  1.238 ns (0 allocations: 0 bytes)
julia> @btime f2((x=1, y=2))
  0.015 ns (0 allocations: 0 bytes)                                                                                                                                                                                                                                                                                                   

julia> @btime f3(XY(1, 2))
  0.015 ns (0 allocations: 0 bytes)

All three cases are 1ns or faster. You may note that f2 and f3 appear to be much faster, but sub-1ns timings just indicate that the compiler has optimized out the entire function, since it can trivially prove that the result is unused. Setting that aside, all three options are sufficiently fast and you should pick whichever fits your use case best.


Thank you for clarification!

By the way, if you want to use a struct but don’t want to have to remember the argument order, I would recommend https://github.com/mauro3/Parameters.jl which makes it easy to define structs with more friendly constructors and default values:

julia> using Parameters

julia> @with_kw struct Foo
         x::Int = 1
         y::Float64 = 2.0

julia> Foo()
  x: Int64 1
  y: Float64 2.0

julia> Foo(y = 3)
  x: Int64 1                                                                                                                                                                                                                                                                                                                          
  y: Float64 3.0 

With Julia 1.1, you probably don’t even need Parameters.jl and could just use Base.@kwdef instead of Parameters.@with_kw.


And the author (mauro3) of Parameters stated that Parameter.jl will align with Base.@kwdef with some API breakage. https://github.com/mauro3/Parameters.jl/issues/121.
There is another package of mauro3, Unpack.jl, doing @unpack a,b,c = p instead of a, b, c = p.a, p.b, p.c for dicts / structs / namedtuples / labelled arrays, which is handy IMHO.