jcz
jcz

Reputation: 267

Is there a function in Julia for dumping the field values of a type into a tuple?

Something like this works

struct MyStruct
    x
    y
    z
end

x = MyStruct(1, 2, 3)

a, b, c = ntuple(i -> getfield(x, fieldnames(MyStruct)[i]), length(fieldnames(MyStruct)))

but I can't help but think I'm just reinventing the wheel.

Upvotes: 1

Views: 290

Answers (2)

Gus
Gus

Reputation: 4505

In addition to the existing answers, there are two alternatives: ntuple and generated functions. A baseline is to manually write the code (which could be generated by a macro).

ntuple usually generates better code, and in this case is more than 20 times faster.

struct Foo{A, B}
    a::A
    b::B
end

dump0(x) = getfield.(Ref(x), fieldnames(typeof(x)))
dump1(x) = ntuple(i -> getfield(x, i), fieldcount(typeof(x)))

# Based off https://discourse.julialang.org/t/slowness-of-fieldnames-and-propertynames/55364/2
@generated function  dump2(obj::T) where {T}
    return :((tuple($((
        :(getfield(obj, $i)) for i in 1:fieldcount(obj)
    )...))))
end

# hardcoded
dump3(x::Foo) = (x.a, x.b)

@code_llvm dump3(Foo(1,2)) and @code_llvm dump2(Foo(1,2)) are the same, whereas @code_llvm dump1(Foo(1,2)) and @code_llvm dump0(Foo(1,2)) are much complicated.

To get timings, define

eq0(a, b) = ==(dump0(a), dump0(b))
eq1(a, b) = ==(dump1(a), dump1(b))
eq2(a, b) = ==(dump2(a), dump2(b))
eq3(a, b) = ==(dump3(a), dump3(b))

(These equality functions will behave differently than the default defined on Foo with respect to missing and NaN. Another motivation for dumping to a tuple is to define e.g. hash(x::Foo, s::UInt) = hash(dump(x), hash(Foo, s))

I was not able to find a performance benchmark that shows a considerable difference between dump1 and the dump2, even though the code generated for dump2 is seemingly more efficient. dump0 is much slower:

julia> b = @benchmarkable eq0(Foo(a, b), Foo(c, d)) setup=((a, b, c, d) = rand(0:5, 4))
Benchmark(evals=1, seconds=5.0, samples=10000)

julia> run(b)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  1.217 μs … 46.291 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.269 μs              ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.417 μs ±  1.145 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%
[...]

 Memory estimate: 448 bytes, allocs estimate: 14.

julia> b = @benchmarkable eq1(Foo(a, b), Foo(c, d)) setup=((a, b, c, d) = rand(0:5, 4))
Benchmark(evals=1, seconds=5.0, samples=10000)

julia> run(b)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  37.000 ns …  18.596 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     41.000 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   43.594 ns ± 185.778 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
[...]

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> b = @benchmarkable eq2(Foo(a, b), Foo(c, d)) setup=((a, b, c, d) = rand(0:5, 4))
Benchmark(evals=1, seconds=5.0, samples=10000)

julia> run(b)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  40.000 ns …  13.651 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     44.000 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   46.200 ns ± 152.346 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
[...]

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> b = @benchmarkable eq3(Foo(a, b), Foo(c, d)) setup=((a, b, c, d) = rand(0:5, 4))
Benchmark(evals=1, seconds=5.0, samples=10000)

julia> run(b)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  40.000 ns … 168.000 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     44.000 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   43.878 ns ±   2.114 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
[...]

 Memory estimate: 0 bytes, allocs estimate: 0.

Upvotes: 0

Bogumił Kamiński
Bogumił Kamiński

Reputation: 69879

You can apply getfield over names of fields like this:

getfield.(Ref(x), fieldnames(typeof(x)))

You can also replace Ref(x) with (x,) or [x] to protect x against being broadcasted over. Here is an example how you could silently get a wrong result:

julia> using NamedArrays

julia> x = NamedArray(fill((array=1, dicts=2, dimnames=3),3))
3-element Named Array{NamedTuple{(:array, :dicts, :dimnames),Tuple{Int64,Int64,Int64}},1}
A  │
───┼─────────────────────────────────────
1  │ (array = 1, dicts = 2, dimnames = 3)
2  │ (array = 1, dicts = 2, dimnames = 3)
3  │ (array = 1, dicts = 2, dimnames = 3)

julia> getfield.(Ref(x), fieldnames(typeof(x))) # correct
(NamedTuple{(:array, :dicts, :dimnames),Tuple{Int64,Int64,Int64}}[(array = 1, dicts = 2, dimnames = 3), (array = 1, dicts = 2, dimnames = 3), (array = 1, dicts = 2, dimnames = 3)], (OrderedCollections.OrderedDict("1"=>1,"2"=>2,"3"=>3),), (:A,))

julia> getfield.(x, fieldnames(typeof(x))) # wrong
3-element Named Array{Int64,1}
A  │
───┼──
1  │ 1
2  │ 2
3  │ 3

Upvotes: 2

Related Questions