Slowness of fieldnames and propertynames

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
2 Likes