I think this might be a related issue: I wanted to convert a struct
into a NamedTuple
(with the keys being the field names of the struct), and while it seems like the compiler has all the info it needs with
named_tuple(obj::T) where {T} = NamedTuple{fieldnames(T), Tuple{fieldtypes(T)...}}(ntuple(i -> getfield(obj, i), fieldcount(T)))
it is much slower than the equivalent generated function
@generated function named_tuple_gen(obj)
NT = NamedTuple{fieldnames(obj), Tuple{fieldtypes(obj)...}}
return :($NT(tuple($((:(getfield(obj, $i)) for i in 1:fieldcount(obj))...))))
end
which I got working by modifying Static fieldnames - #2 by yuyichao; thanks for the link! (I actually had to expand it out as
@generated function named_tuple_gen(obj::T) where {T}
NT = NamedTuple{fieldnames(obj), Tuple{fieldtypes(obj)...}}
return :(
$NT(
tuple(
$(
(
:(getfield(obj, $i)) for i in 1:fieldcount(obj)
)...
)
)
)
)
end
to see what’s going on better; VSCode makes little vertical lines which makes it a bit easier to parse the paren alignment than here, though).
It seemed like I could recover some of the performance without generated functions if I wanted it just for a specific type T
by doing
const NT = NamedTuple{fieldnames(T), Tuple{fieldtypes(T)...}}
T_as_NT(obj::T) = NT(ntuple(i -> getfield(obj, i), fieldcount(T)))
which at least lets it infer the output type correctly, though if there are more than 10 fields it can’t infer the output of ntuple
. (It seems basically as fast as the generated function for fewer than 10 fields; the cutoff of course is from julia/ntuple.jl at a319ae45a15fb005c7d7e29e3f3a62f27e18e3a6 · JuliaLang/julia · GitHub).
Example with 11 fields
julia> using BenchmarkTools
julia> named_tuple(obj::T) where {T} = NamedTuple{fieldnames(T), Tuple{fieldtypes(T)...}}(ntuple(i -> getfield(obj, i), fieldcount(T)))
named_tuple (generic function with 1 method)
julia> @generated function named_tuple_gen(obj::T) where {T}
NT = NamedTuple{fieldnames(obj), Tuple{fieldtypes(obj)...}}
return :($NT(tuple($((:(getfield(obj, $i)) for i in 1:fieldcount(obj))...))))
end
named_tuple_gen (generic function with 1 method)
julia> struct ManyBools
x1::Bool
x2::Bool
x3::Bool
x4::Bool
x5::Bool
x6::Bool
x7::Bool
x8::Bool
x9::Bool
x10::Bool
x11::Bool
end
julia> const NT = NamedTuple{fieldnames(ManyBools), Tuple{fieldtypes(ManyBools)...}}
NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9, :x10, :x11), NTuple{11, Bool}}
julia> ManyBools_as_NT(obj::ManyBools) = NT(ntuple(i -> getfield(obj, i), fieldcount(ManyBools)))
ManyBools_as_NT (generic function with 1 method)
julia> obj = ManyBools(rand(Bool, 11)...)
ManyBools(false, false, false, false, false, true, false, true, false, false, true)
julia> @benchmark named_tuple($(Ref(obj))[])
BenchmarkTools.Trial:
memory estimate: 528 bytes
allocs estimate: 6
--------------
minimum time: 2.768 μs (0.00% GC)
median time: 2.806 μs (0.00% GC)
mean time: 2.856 μs (0.75% GC)
maximum time: 218.773 μs (98.07% GC)
--------------
samples: 10000
evals/sample: 9
julia> @benchmark named_tuple_gen($(Ref(obj))[])
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 2.125 ns (0.00% GC)
median time: 2.250 ns (0.00% GC)
mean time: 2.257 ns (0.00% GC)
maximum time: 23.250 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
julia> @benchmark ManyBools_as_NT($(Ref(obj))[])
BenchmarkTools.Trial:
memory estimate: 128 bytes
allocs estimate: 2
--------------
minimum time: 351.827 ns (0.00% GC)
median time: 357.280 ns (0.00% GC)
mean time: 368.931 ns (1.09% GC)
maximum time: 9.198 μs (95.65% GC)
--------------
samples: 10000
evals/sample: 214
julia> @code_warntype named_tuple(obj)
Variables
#self#::Core.Const(named_tuple)
obj::ManyBools
#5::var"#5#6"{ManyBools}
Body::NamedTuple
1 ─ %1 = Main.fieldnames($(Expr(:static_parameter, 1)))::Tuple{Vararg{Symbol, N} where N}
│ %2 = Core.tuple(Main.Tuple)::Core.Const((Tuple,))
│ %3 = Main.fieldtypes($(Expr(:static_parameter, 1)))::Tuple
│ %4 = Core._apply_iterate(Base.iterate, Core.apply_type, %2, %3)::Type
│ %5 = Core.apply_type(Main.NamedTuple, %1, %4)::Type{NamedTuple{_A, _B}} where {_A, _B}
│ %6 = Main.:(var"#5#6")::Core.Const(var"#5#6")
│ %7 = Core.typeof(obj)::Core.Const(ManyBools)
│ %8 = Core.apply_type(%6, %7)::Core.Const(var"#5#6"{ManyBools})
│ (#5 = %new(%8, obj))
│ %10 = #5::var"#5#6"{ManyBools}
│ %11 = Main.fieldcount($(Expr(:static_parameter, 1)))::Core.Const(11)
│ %12 = Main.ntuple(%10, %11)::Tuple{Vararg{Bool, N} where N}
│ %13 = (%5)(%12)::NamedTuple
└── return %13
julia> @code_warntype named_tuple_gen(obj)
Variables
#self#::Core.Const(named_tuple_gen)
obj::ManyBools
Body::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9, :x10, :x11), NTuple{11, Bool}}
1 ─ %1 = Main.getfield(obj, 1)::Bool
│ %2 = Main.getfield(obj, 2)::Bool
│ %3 = Main.getfield(obj, 3)::Bool
│ %4 = Main.getfield(obj, 4)::Bool
│ %5 = Main.getfield(obj, 5)::Bool
│ %6 = Main.getfield(obj, 6)::Bool
│ %7 = Main.getfield(obj, 7)::Bool
│ %8 = Main.getfield(obj, 8)::Bool
│ %9 = Main.getfield(obj, 9)::Bool
│ %10 = Main.getfield(obj, 10)::Bool
│ %11 = Main.getfield(obj, 11)::Bool
│ %12 = Main.tuple(%1, %2, %3, %4, %5, %6, %7, %8, %9, %10, %11)::NTuple{11, Bool}
│ %13 = (NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9, :x10, :x11), NTuple{11, Bool}})(%12)::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9, :x10, :x11), NTuple{11, Bool}}
└── return %13
julia> @code_warntype ManyBools_as_NT(obj)
Variables
#self#::Core.Const(ManyBools_as_NT)
obj::ManyBools
#10::var"#10#11"{ManyBools}
Body::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9, :x10, :x11), NTuple{11, Bool}}
1 ─ %1 = Main.:(var"#10#11")::Core.Const(var"#10#11")
│ %2 = Core.typeof(obj)::Core.Const(ManyBools)
│ %3 = Core.apply_type(%1, %2)::Core.Const(var"#10#11"{ManyBools})
│ (#10 = %new(%3, obj))
│ %5 = #10::var"#10#11"{ManyBools}
│ %6 = Main.fieldcount(Main.ManyBools)::Core.Const(11)
│ %7 = Main.ntuple(%5, %6)::Tuple{Vararg{Bool, N} where N}
│ %8 = Main.NT(%7)::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9, :x10, :x11), NTuple{11, Bool}}
└── return %8
Example with 9 fields
julia> using BenchmarkTools
julia> named_tuple(obj::T) where {T} = NamedTuple{fieldnames(T), Tuple{fieldtypes(T)...}}(ntuple(i -> getfield(obj, i), fieldcount(T)))
named_tuple (generic function with 1 method)
julia> @generated function named_tuple_gen(obj::T) where {T}
NT = NamedTuple{fieldnames(obj), Tuple{fieldtypes(obj)...}}
return :($NT(tuple($((:(getfield(obj, $i)) for i in 1:fieldcount(obj))...))))
end
named_tuple_gen (generic function with 1 method)
julia> struct FewerBools
x1::Bool
x2::Bool
x3::Bool
x4::Bool
x5::Bool
x6::Bool
x7::Bool
x8::Bool
x9::Bool
end
julia> const NT = NamedTuple{fieldnames(FewerBools), Tuple{fieldtypes(FewerBools)...}}
NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9), NTuple{9, Bool}}
julia> FewerBools_as_NT(obj::FewerBools) = NT(ntuple(i -> getfield(obj, i), fieldcount(FewerBools)))
FewerBools_as_NT (generic function with 1 method)
julia> obj = FewerBools(rand(Bool, 9)...)
FewerBools(false, true, false, true, true, true, false, true, false)
julia> @benchmark named_tuple($(Ref(obj))[])
BenchmarkTools.Trial:
memory estimate: 384 bytes
allocs estimate: 5
--------------
minimum time: 2.083 μs (0.00% GC)
median time: 2.120 μs (0.00% GC)
mean time: 2.152 μs (0.00% GC)
maximum time: 4.940 μs (0.00% GC)
--------------
samples: 10000
evals/sample: 9
julia> @benchmark named_tuple_gen($(Ref(obj))[])
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 1.541 ns (0.00% GC)
median time: 1.583 ns (0.00% GC)
mean time: 1.590 ns (0.00% GC)
maximum time: 5.791 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
julia> @benchmark FewerBools_as_NT($(Ref(obj))[])
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 1.833 ns (0.00% GC)
median time: 1.917 ns (0.00% GC)
mean time: 1.948 ns (0.00% GC)
maximum time: 15.917 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
julia> @code_warntype named_tuple(obj)
Variables
#self#::Core.Const(named_tuple)
obj::FewerBools
#5::var"#5#6"{FewerBools}
Body::NamedTuple
1 ─ %1 = Main.fieldnames($(Expr(:static_parameter, 1)))::Tuple{Vararg{Symbol, N} where N}
│ %2 = Core.tuple(Main.Tuple)::Core.Const((Tuple,))
│ %3 = Main.fieldtypes($(Expr(:static_parameter, 1)))::Tuple
│ %4 = Core._apply_iterate(Base.iterate, Core.apply_type, %2, %3)::Type
│ %5 = Core.apply_type(Main.NamedTuple, %1, %4)::Type{NamedTuple{_A, _B}} where {_A, _B}
│ %6 = Main.:(var"#5#6")::Core.Const(var"#5#6")
│ %7 = Core.typeof(obj)::Core.Const(FewerBools)
│ %8 = Core.apply_type(%6, %7)::Core.Const(var"#5#6"{FewerBools})
│ (#5 = %new(%8, obj))
│ %10 = #5::var"#5#6"{FewerBools}
│ %11 = Main.fieldcount($(Expr(:static_parameter, 1)))::Core.Const(9)
│ %12 = Main.ntuple(%10, %11)::NTuple{9, Bool}
│ %13 = (%5)(%12)::NamedTuple
└── return %13
julia> @code_warntype named_tuple_gen(obj)
Variables
#self#::Core.Const(named_tuple_gen)
obj::FewerBools
Body::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9), NTuple{9, Bool}}
1 ─ %1 = Main.getfield(obj, 1)::Bool
│ %2 = Main.getfield(obj, 2)::Bool
│ %3 = Main.getfield(obj, 3)::Bool
│ %4 = Main.getfield(obj, 4)::Bool
│ %5 = Main.getfield(obj, 5)::Bool
│ %6 = Main.getfield(obj, 6)::Bool
│ %7 = Main.getfield(obj, 7)::Bool
│ %8 = Main.getfield(obj, 8)::Bool
│ %9 = Main.getfield(obj, 9)::Bool
│ %10 = Main.tuple(%1, %2, %3, %4, %5, %6, %7, %8, %9)::NTuple{9, Bool}
│ %11 = (NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9), NTuple{9, Bool}})(%10)::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9), NTuple{9, Bool}}
└── return %11
julia> @code_warntype FewerBools_as_NT(obj)
Variables
#self#::Core.Const(FewerBools_as_NT)
obj::FewerBools
#10::var"#10#11"{FewerBools}
Body::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9), NTuple{9, Bool}}
1 ─ %1 = Main.:(var"#10#11")::Core.Const(var"#10#11")
│ %2 = Core.typeof(obj)::Core.Const(FewerBools)
│ %3 = Core.apply_type(%1, %2)::Core.Const(var"#10#11"{FewerBools})
│ (#10 = %new(%3, obj))
│ %5 = #10::var"#10#11"{FewerBools}
│ %6 = Main.fieldcount(Main.FewerBools)::Core.Const(9)
│ %7 = Main.ntuple(%5, %6)::NTuple{9, Bool}
│ %8 = Main.NT(%7)::NamedTuple{(:x1, :x2, :x3, :x4, :x5, :x6, :x7, :x8, :x9), NTuple{9, Bool}}
└── return %8