Making an iterator that splats keys and values quickly into kwargs

Oh wow.

So trying this approach to make one-line solutions for the original problem, using a Generator becomes hugely slow! What the heck! The same approach which previously was quite performant, is now painfully slow.

Setup:

using BenchmarkTools
struct MyStruct{K1,V1,K2,V2}
    a::NamedTuple{K1,V1}
    b::NamedTuple{K2,V2}
end
x = MyStruct((a=1, b=2, d=4, e=5, f=6), (b=3, c=4))

First, making one-line solutions using map and filter to set a speed baseline:

Base.keys(x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2} = 
    (K1..., filter(k->k ∉ K1, K2)...)
Base.values(x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2} = 
    (values(x.a)..., map(Base.Fix1(getfield, x.b), filter(k->k ∉ K1, K2))...)
Base.merge(nt::NamedTuple, x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2} = 
    (; nt..., NamedTuple{keys(x)}(values(x))...)

Runtime with map and filter:

julia> @btime keys($x)
  1.000 ns (0 allocations: 0 bytes)
(:a, :b, :d, :e, :f, :c)

julia> @btime values($x)
  2.300 ns (0 allocations: 0 bytes)
(1, 2, 4, 5, 6, 4)

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

Nice and fast.

Okay, now trying one-line solutions using generators, code is almost identical:

Base.keys(x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2} = 
    (K1..., (k for k ∈ K2 if k ∉ K1)...)
Base.values(x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2} = 
    (values(x.a)..., (getfield(x.b,k) for k ∈ K2 if k ∉ K1)...)
Base.merge(nt::NamedTuple, x::MyStruct{K1,<:Any,K2,<:Any}) where {K1,K2} = 
    (; nt..., NamedTuple{keys(x)}(values(x))...)

Runtime with Generators:

julia> @btime keys($x)
  309.045 ns (2 allocations: 96 bytes)
(:a, :b, :d, :e, :f, :c)

julia> @btime values($x)
  410.326 ns (5 allocations: 256 bytes)
(1, 2, 4, 5, 6, 4)

julia> @btime (; $x...)
  1.480 μs (11 allocations: 560 bytes)
(a = 1, b = 2, d = 4, e = 5, f = 6, c = 4)

Oof! Generators are slow af!

If I’m missing something, I don’t see it… it just seems like generators sometimes just become slow without rhyme or reason.

Considering that generators are themselves just syntax sugar for map and filter over iterators, this shouldn’t be the case?