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?