Overloading `getproperty` in a type-stable manner

Hi, I am trying to overload getproperty for a struct I have.

I do:

function Base.getproperty(m::ABM, s::Symbol)
    if s ∈ (:agents, :space, :scheduler, :properties)
        return Base.getfield(m, s)
    else
        return Base.getindex(m.properties, s)
    end
end

So, my end goal is to still be able to write model.agents and get the actual field of the struct, but if I type model.s to get the value of [:s] from the underlying dictionary. The method as I have written it above works just fine and does what it should.

Now, before adding this method, I do:


julia> @btime $(model).properties[:Ο΅]
  9.000 ns (0 allocations: 0 bytes)
0.4

julia> @btime $(model).scheduler
  0.001 ns (0 allocations: 0 bytes)
fastest (generic function with 1 method)

julia> @code_warntype model.properties[:Ο΅]
Variables
  #self#::Core.Compiler.Const(getindex, false)
  h::Dict{Symbol,Float64}
  key::Symbol
  val::Union{}
  index::Int64

Body::Float64
1 ─       Core.NewvarNode(:(val))
β”‚         (index = Base.ht_keyindex(h, key))
β”‚         $(Expr(:inbounds, true))
β”‚   %4  = (index < 0)::Bool
└──       goto #3 if not %4
2 ─ %6  = Base.KeyError(key)::KeyError
β”‚         Base.throw(%6)
└──       Core.Compiler.Const(:(return %7), false)
3 β”„ %9  = Base.getproperty(h, :vals)::Array{Float64,1}
β”‚   %10 = Base.getindex(%9, index)::Float64
β”‚   %11 = Core.typeassert(%10, $(Expr(:static_parameter, 2)))::Float64
└──       return %11
4 ─       $(Expr(:inbounds, :pop))
└──       Core.Compiler.Const(:(return val), false)

I then add the method, and I have:

julia> @btime $(model).scheduler
  17.735 ns (0 allocations: 0 bytes)
fastest (generic function with 1 method)

julia> @btime $(model).properties[:Ο΅]
  49.545 ns (1 allocation: 16 bytes)
0.4

julia> @btime $(model).Ο΅
  49.848 ns (1 allocation: 16 bytes)
0.4

julia> @code_warntype model.Ο΅
Variables
  #self#::Core.Compiler.Const(getproperty, false)
  m::AgentBasedModel{HKAgent,Nothing,typeof(fastest),Dict{Symbol,Float64}}   
  s::Symbol

Body::Any
1 ─ %1 = (:agents, :space, :scheduler, :properties)::Core.Compiler.Const((:agents, :space, :scheduler, :properties), false)
β”‚   %2 = (s ∈ %1)::Bool
└──      goto #3 if not %2
2 ─ %4 = Base.getfield::Core.Compiler.Const(getfield, false)
β”‚   %5 = (%4)(m, s)::Union{typeof(fastest), Nothing, Dict}
└──      return %5
3 ─ %7 = Base.getindex::Core.Compiler.Const(getindex, false)
β”‚   %8 = Base.getproperty(m, :properties)::Any
β”‚   %9 = (%7)(%8, s)::Any
└──      return %9

Performance dropped and I have type instability. Is there a way to achieve what I want, or is it strictly impossible to use getproperty to do both getfield as well as something else?

Fredrik suggested that I try instead

function Base.getproperty(m::ABM{A, S, F, P}, s::Symbol) where {A, S, F, P}
    if s === :agents
        return Base.getfield(m, :agents)
    elseif s === :space
        return Base.getfield(m, :space)
    elseif s === :scheduler
        return Base.getfield(m, :scheduler)
    elseif s === :properties
        return Base.getfield(m, :properties)
    elseif P <: Dict
        return Base.getindex(m.properties, s)
    else # properties is assumed to be a struct
        return Base.getproperty(m.properties, s)
    end
end

and test using functions, so that constant propagation occurs. So I did:

test1(model) = model.a
@inferred test1(model1)

but this also didn’t infer: ERROR: return type Float64 does not match inferred return type Any

I would try replacing m.properties on the 5th line with getfield(m, :properties). This recursion could be the reason why your getproperty is not inferred here.

2 Likes

Update!

using:

function Base.getproperty(m::ABM{A, S, F, P}, s::Symbol) where {A, S, F, P}
    if s === :agents
        return getfield(m, :agents)
    elseif s === :space
        return getfield(m, :space)
    elseif s === :scheduler
        return getfield(m, :scheduler)
    elseif s === :properties
        return getfield(m, :properties)
    elseif P <: Dict
        return getindex(getfield(m, :properties), s)
    else # properties is assumed to be a struct
        return getproperty(getfield(m, :properties), s)
    end
end

and restarting Julia to clear everything works, but one must be sure to test this inside functions so that constant propagation occurs!

@simeonschaub Hah I just saw your answer! You say the same thing!

1 Like
4 Likes