# Efficient reflection on structs

**URL:** <https://discourse.julialang.org/t/efficient-reflection-on-structs/55278>\
**Category:** Performance\
**Created:** [February 14, 2021, 9:24pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278 "2021-02-14T21:24:53Z")\
**Posts on this page:** 15\
**Page:** 3

<div class="post-metadata">

**Author:** ![Guillaume\_Leclerc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guillaume_leclerc/32/21940_2.png) [@Guillaume\_Leclerc](https://discourse.julialang.org/u/Guillaume_Leclerc)\
**Post date:** [February 16, 2021, 5:20pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/41 "2021-02-16T17:20:02Z")

</div>

> [@JeffreySarnoff](#):
>
> `isa` is a _built-in function_ – none of those functions are overrideable.  
> You are welcome to fashion your own, using `isa` and override that.  
> Your backstop would be something like this.
> 
> ```julia
> isit(x, ::Type{T}) where T = isa(x, T)
> 
> ```

Does that mean I’m not allowed to call implementations of functions that accept `T` with a `Wrapper{T}` as an argument ?

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [February 16, 2021, 5:49pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/42 "2021-02-16T17:49:13Z")

</div>

> [@Guillaume\_Leclerc](#):
>
> I looked at `llvm_code` and it is going through the iterator in the second case. For some reason the compiler doesn’t unroll the loop. even if I hard code the fields instead of reading them from the type parameter I get the same slowdown.
> 
> Here is my tuple recursion implementation (I couldn’t find the `head` , `tail` syntax in julia)

It’s not actually a question of unrolling, but rather a question of the `fieldnames` being known at compile time. For some reason, the compiler devs have decided that `fieldnames` shouldn’t be treated as a pure function by the compiler, even though it’s information that cannot be changed.

So unfortunately, you will need to use a generated function for this if you want efficient behaviour. Just _obtaining_ the fieldnames is slow.

```julia
julia> struct Foo
           f1::Int
           f2::Int
           f4::Int
       end

julia> @btime fieldnames($Foo)
  389.599 ns (1 allocation: 32 bytes)
(:f1, :f2, :f4)

```

A change to this was proposed in [Pure fieldnames by bramtayl · Pull Request #30152 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/pull/30152), but one point that was raised against it was

> We discourage usage of this API, and generally state that the fields of a type are an internal/private property in most cases and that users should define a function-based API (incl. accessors, getindex/keys, getproperty/propertynames, etc.)

Personally, I think this should be revisited, but in the meantime you’ll either need a generated function to make this work, or to change your approach.

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [February 16, 2021, 5:53pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/43 "2021-02-16T17:53:21Z")

</div>

> [@Guillaume\_Leclerc](#):
>
> Is there a way I could get something like this:
> 
> ```julia
> struct Wrapper{T, F} <: T
> data::T
> end
> 
> ```
> 
> This way I would be able to reuse all the implementations available for `T` . Since it has the same fields (accessible through `getproperty` ) it should be swappable in place of any variable of type `T` .
> 
> Am I allowed to override `isa` or something along those lines ?

No, this is not possible. However, there are a few classes of circumstances where people desire this behaviour: [Is Julia's way of OOP superior to C++/Python? Why Julia doesn't use class-based OOP? - #137 by Mason](https://discourse.julialang.org/t/is-julias-way-of-oop-superior-to-c-python-why-julia-doesnt-use-class-based-oop/52058/137)

One can kinda hack their way into this sort of behaviour using IRTools.jl, but I wouldn’t recommend it.

---

<div class="post-metadata">

**Author:** ![Guillaume\_Leclerc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guillaume_leclerc/32/21940_2.png) [@Guillaume\_Leclerc](https://discourse.julialang.org/u/Guillaume_Leclerc)\
**Post date:** [February 16, 2021, 6:16pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/44 "2021-02-16T18:16:50Z")

</div>

But why is it slow when I hardcode the fields too then?

I.e:

```julia
myCopyInner(target, source, :a, :b)

```

It’s not even using fieldtypes. It doesn’t really matter since code generation worked very well for me I’m just trying to understand the compilation process.

---

<div class="post-metadata">

**Author:** ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)\
**Post date:** [February 16, 2021, 6:27pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/45 "2021-02-16T18:27:03Z")

</div>

> [@Guillaume\_Leclerc](#):
>
> Does that mean I’m not allowed to call implementations of functions that accept `T` with a `Wrapper{T}` as an argument ?

I am probably missing the point here again (don’t be afraid to just ignore me 🙂 I am aware I am not up to the level of the discussion here). But maybe something in the lines of the `FieldVector` implementation of `StaticArrays` is what you are searching for?

> **[API · StaticArrays.jl](https://juliaarrays.github.io/StaticArrays.jl/stable/pages/api/#StaticArrays.FieldVector)**
>
> Documentation for StaticArrays.jl.

It wraps a user-defined immutable struct to a specific type for which all functions are defined, and in an efficient manner, because the memory layouts are the same.

---

<div class="post-metadata">

**Author:** ![Guillaume\_Leclerc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guillaume_leclerc/32/21940_2.png) [@Guillaume\_Leclerc](https://discourse.julialang.org/u/Guillaume_Leclerc)\
**Post date:** [February 16, 2021, 6:36pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/46 "2021-02-16T18:36:08Z")

</div>

That could be helpful. The problem is my structs also contain (sometimes very large) static arrays. And compilation time for StaticArray is prohibitive 😞. I know that they are doing code gen but I don’t need all these features.

At the end of the day I also need to store and n-dimension array of these while being able to control the memory layout to ensure I have proper alignment and coalescing to yield max memory bandwidth on CUDA. So I assumed there would not be something already implemented for this particular use case.

---

<div class="post-metadata">

**Author:** ![JeffreySarnoff](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jeffreysarnoff/32/1980_2.png) [@JeffreySarnoff](https://discourse.julialang.org/u/JeffreySarnoff)\
**Post date:** [February 16, 2021, 6:40pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/47 "2021-02-16T18:40:41Z")

</div>

`isit` should be just as capable as `isa` for your purposes,  
and [at least very nearly] equally fast.

---

<div class="post-metadata">

**Author:** ![Guillaume\_Leclerc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guillaume_leclerc/32/21940_2.png) [@Guillaume\_Leclerc](https://discourse.julialang.org/u/Guillaume_Leclerc)\
**Post date:** [February 16, 2021, 6:46pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/48 "2021-02-16T18:46:40Z")

</div>

But it wouldn’t allow dispatching on function on `Wrapper{T}` would it ?

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [February 16, 2021, 8:45pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/49 "2021-02-16T20:45:25Z")

</div>

> [@Guillaume\_Leclerc](#):
>
> But why is it slow when I hardcode the fields too then?
> 
> I.e:
> 
> ```julia
> myCopyInner(target, source, :a, :b)
> 
> ```
> 
> It’s not even using fieldtypes. It doesn’t really matter since code generation worked very well for me I’m just trying to understand the compilation process.

You’re right, I spoke too soon. I found a couple small issues in your implementation above, but I was unable to get anywhere near the performance of a generated function here using tuple recursion

```julia
struct Wrapper{T, F}
    data::T
end

function wrap(data::T) where T
    fnames = fieldnames(T)
    sizes = map(x -> sizeof(fieldtype(T, x)), fnames)
    F = NamedTuple{fnames}(sizes)
    Wrapper{T, F}(data)
end

function easyCreate(T)
    v = T(map(x -> zero(fieldtype(T, x)), fieldnames(T))...)
    wrap(v)
end

function Base.getproperty(wrapper::Wrapper, s::Symbol)
    getproperty(getfield(wrapper, :data), s)
end

function Base.setproperty!(wrapper::Wrapper, s::Symbol, v)
    setproperty!(getfield(wrapper, :data), s, v)
end

#---------------------------------------------

function copyto_loop!(target::Wrapper{T, F}, source::Wrapper{T, F}) where {T, F}
    for property in keys(F)
        setproperty!(target, property, getproperty(source, property))
    end
    target
end 

#---------------------------------------------

function copyto_recursive!(target::Wrapper{T, F}, source::Wrapper{T, F}) where {T, F}
    copyto_recursive!(target, source, keys(F))
    target
end

@inline function copyto_recursive!(target::Wrapper{T, F}, source::Wrapper{T, F}, 
                                   properties::NTuple{N, Symbol}) where {T, F, N}
    N == 0 && return nothing
    property = first(properties)
    setproperty!(target, property, getproperty(source, property))
    copyto_recursive!(target, source, Base.tail(properties))
end

#---------------------------------------------

@generated function copyto_generated!(target::Wrapper{T, F}, source::Wrapper{T, F}) where {T, F}
    qproperties = QuoteNode.(keys(F))
    Expr(:block, 
         (:(setproperty!(target, $property, getproperty(source, $property))) for property in qproperties)..., 
         :target)
end

```

```julia
using BenchmarkTools

mutable struct Data2
    a::Int
    b::Float32
end

data1 = easyCreate(Data)
data2 = easyCreate(Data2)

@btime ($data2.a = $data2.a; $data2.b = $data2.b; )
@btime copyto_loop!($data2, $data2)
@btime copyto_recursive!($data2, $data2)
@btime copyto_generated!($data2, $data2);
#+end_src

#+RESULTS:
   2.069 ns (0 allocations: 0 bytes)
   111.080 ns (2 allocations: 32 bytes)
   75.495 ns (1 allocation: 16 bytes)
   1.869 ns (0 allocations: 0 bytes)

```

It seems that the optimizer is just not willing to do the amount of inlining or analysis required to make the loop or recursive versions fast, and we just have to reach in with a generated function and manually tell it what code to generate.

This is a little disappointing because fundamentally, the required information is present in the type signature of `Wrapper` as you pointed out.

@tim.holy is a big advocate for using Tuple recursion instead of generated functions, so I’ll ping him on the off chance he has the time to look and sees what’s wrong with `copyto_recursive!`.

---

<div class="post-metadata">

**Author:** ![Guillaume\_Leclerc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guillaume_leclerc/32/21940_2.png) [@Guillaume\_Leclerc](https://discourse.julialang.org/u/Guillaume_Leclerc)\
**Post date:** [February 16, 2021, 10:05pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/50 "2021-02-16T22:05:28Z")

</div>

Whoa, thanks for spending all this time on this!

At least I’m glad to know at least I wasn’t missing something completely obvious. It’s a bit sad thought indeed. Like with `fieldnames` that could be reasonable. The fact that it’s not unrolling a loop over a 2-uple is definitely disappointing. On the other side I love how macros are implemented and easy to use in Julia so I guess it makes up for it 🙂

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [February 16, 2021, 10:16pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/51 "2021-02-16T22:16:40Z")

</div>

> [@Guillaume\_Leclerc](#):
>
> Whoa, thanks for spending all this time on this!

Happy to help. I like delving into these cornercases.

> [@Guillaume\_Leclerc](#):
>
> The fact that it’s not unrolling a loop over a 2-uple is definitely disappointing.

I believe the lack of unrolling is a symptom of the problem, not the cause of the problem. Something is blocking the constant prop or another optimization pass and it’s not clear what.

---

<div class="post-metadata">

**Author:** ![JeffreySarnoff](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jeffreysarnoff/32/1980_2.png) [@JeffreySarnoff](https://discourse.julialang.org/u/JeffreySarnoff)\
**Post date:** [February 16, 2021, 10:19pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/52 "2021-02-16T22:19:09Z")

</div>

thank you for bringing this to our attention

---

<div class="post-metadata">

**Author:** ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)\
**Post date:** [February 17, 2021, 11:44am UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/53 "2021-02-17T11:44:52Z")

</div>

I didn’t take a close look, but we may not const-prop the values.

You could try doing the recursion in the type domain, `Tuple{:a, :b, :c}` using `Base.tuple_type_head` and `Base.tuple_type_tail`.

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [February 17, 2021, 3:38pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/54 "2021-02-17T15:38:54Z")

</div>

Yep, that did the trick! This is a neat tool to know about, thanks.

```julia
Keys(::NamedTuple{K}) where {K} = Tuple{K...}

function copyto_type_recursive!(target::Wrapper{T, F}, source::Wrapper{T, F}) where {T, F}
    copyto_type_recursive!(target, source, Keys(F))
    target
end

@inline function copyto_type_recursive!(target::Wrapper{T, F}, source::Wrapper{T, F}, 
                                   ::Type{Tup}) where {T, F, Tup <: Tuple}
    Tup === Tuple{} && return nothing
    property = Base.tuple_type_head(Tup)
    setproperty!(target, property, getproperty(source, property))
    copyto_type_recursive!(target, source, Base.tuple_type_tail(Tup))
end

```

```julia
@btime copyto_loop!($data2, $data2)
@btime copyto_recursive!($data2, $data2)
@btime copyto_type_recursive!($data2, $data2)
@btime copyto_generated!($data2, $data2);

#+RESULTS:
   124.987 ns (2 allocations: 32 bytes)
   74.324 ns (1 allocation: 16 bytes)
   1.860 ns (0 allocations: 0 bytes)
   1.820 ns (0 allocations: 0 bytes)

```

Must just have been too much trampolining between types and values I guess?

---

<div class="post-metadata">

**Author:** ![Guillaume\_Leclerc](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guillaume_leclerc/32/21940_2.png) [@Guillaume\_Leclerc](https://discourse.julialang.org/u/Guillaume_Leclerc)\
**Post date:** [February 17, 2021, 6:08pm UTC](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278/55 "2021-02-17T18:08:43Z")

</div>

That looks cool!. I’m not 100% sure this is more readable than the macro, though. But definitely good to know.

[Previous page](https://discourse.julialang.org/t/efficient-reflection-on-structs/55278.md?page=2)
