Making an iterator that splats keys and values quickly into kwargs

union is doing something very similar to what I commented on for Dict in your last issue.

It splats arguments into a vector, which has to be some allocated bit of memory with a length only known at runtime, then splat that back into a tuple with length that needs to be known at compile time.

Better to keep everything in tuples, use functional transformations of the tuples, and let it all happen at compile time. This compiles away:

function Base.keys(x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2}
    reduce(K2; init=K1) do acc, k
        k in acc ? acc : (acc..., k)
    end
end

x = MyStruct((a=1, b=2), (b=3, c=4))
using BenchmarkTools
@btime keys($x)
julia> @btime keys($x)
  1.750 ns (0 allocations: 0 bytes)
(:a, :b, :c)

And instead of using iterate at all, we can just define merge directly for MyStruct (and values, which is also useful):

x = MyStruct((a=1, b=2, d=4, e=5, f=6), (b=3, c=4))
function Base.values(x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2}
    reduce(K2; init=values(x.a)) do acc, k
        k in K1 ? acc : (acc..., getfield(x.b, k))
    end
end
function Base.merge(nt::NamedTuple, x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2}
    (; nt..., NamedTuple{keys(x)}(values(x))...)
end

@btime (; $x...)
  3.480 ns (0 allocations: 0 bytes)
(a = 1, b = 2, d = 4, e = 5, f = 6, c = 4)

Edit: and kwargs benchmarks now match args

julia> @btime let; [(MyStruct((a=rand(),b=rand()),(c=rand(),d=rand()))...,) for i=1:1000] end;
  13.198 μs (2 allocations: 31.30 KiB)
julia> @btime let; [(; MyStruct((a=rand(),b=rand()),(c=rand(),d=rand()))...,) for i=1:1000] end;
  13.183 μs (2 allocations: 31.30 KiB)
5 Likes