Fragile type inference in custom getproperty()

In a much larger code, we’re having some issues with type inference in a custom getproperty() function. Here’s a minimal example demonstrating at least part of the problem:

mutable struct MyElement{T}
    name :: String
    value :: T
    _filled::Set{Symbol}
    _frozen::Bool
    _in_expression::Vector{Symbol}
end

function Base.getproperty(elm::MyElement, field::Symbol)
    return _getproperty(elm, field)
end

function _getproperty(elm::MyElement, field::Symbol)
    value = getfield(elm, field)
    return customget(elm, field, value)
end

function customget(elm::MyElement, field::Symbol, value)
    if field in (:value, :other)
        return value
    else
        return value
    end

end

This is obviously contrived and our actual code has a series of other checks and such, but I’ve stripped that all out. Nevertheless, if I run in Julia 1.11.1:

using BenchmarkTools
f(elm) = elm.value

elm = MyElement("", 1.0, Set([:value]),false, Symbol[])
@btime f($elm)

I get 26.525 ns (1 allocation: 16 bytes). Cthulhu analysis reveals that the compiler has trouble inferring the type of value in _getproperty here, but I can’t figure out why. If I make even minor changes the code, it works fine. Things I’ve tried that result in proper inference (~2 ns and 0 allocations) include:

  • Removing any of the other fields from the definition of MyElement
  • Combining Base.getproperty and _getproperty or _getproperty and customget
  • Tagging _getproperty with @inline

So I have two questions:

  1. Why is the code above type unstable?
  2. Why is the code inference so fragile to minor changes?

In principle, these functions are inherently type unstable, in the sense that you need to know the value of the propertyname in order to figure out the return type. Of course, in the specific case of getproperty, that would be terrible, since these functions are always called like that. For this reason, the compiler has special rules for getproperty, effectively telling it to always try to constant-propagate. If the property name is a compile-time constant, the functions become type stable again.
This special behavior is reserved for getproperty and does not automatically carry over to your own _getproperty functions, making them type-unstable again. Typically, you can resolve this by telling the compiler to try and propagate constants: (Base.@constprop) Essentials · The Julia Language.
Alternatively, inlining the functions also migrates them back to the constant-propagated-getproperty. Finally, the compiler may decide on it’s own that this function is worth constant propagating, or inlining, depending on compiler heuristics.

1 Like