I am a bit confused how the use of Base.getproperty
is type stable. I understand that this is due to constant propagation, but I am confused how that works in this case.
Why can Julia figure out the type in the second case but not the first case?
julia> @code_warntype (a = 5, b = 5.0).a
MethodInstance for getproperty(::NamedTuple{(:a, :b), Tuple{Int64, Float64}}, ::Symbol)
from getproperty(x, f::Symbol) in Base at Base.jl:42
Arguments
#self#::Core.Const(getproperty)
x::NamedTuple{(:a, :b), Tuple{Int64, Float64}}
f::Symbol
Body::Union{Float64, Int64}
1 ─ nothing
│ %2 = Base.getfield(x, f)::Union{Float64, Int64}
└── return %2
julia> @code_warntype (x->x.a)((a = 5, b = 5.0))
MethodInstance for (::var"#21#22")(::NamedTuple{(:a, :b), Tuple{Int64, Float64}})
from (::var"#21#22")(x) in Main at REPL[36]:1
Arguments
#self#::Core.Const(var"#21#22"())
x::NamedTuple{(:a, :b), Tuple{Int64, Float64}}
Body::Int64
1 ─ %1 = Base.getproperty(x, :a)::Int64
└── return %1
That is a really good question. I've been staring at this for 5 minutes with my compiler theory background, and I'd expect the latter to have a more difficult time. I'm fairly new to Julia's compiler, only looked at certain parts of it, but it seems like this deserves an issue the harder I stare at it.
My rough understanding is that in the first case we're looking at Base.getproperty
itself. In the second case, we've wrapped it into an anonymous function. By wrapping it into an anonymous function, we've allowed constant propagation to occur.
Is there a special case within the compiler forBase.getproperty
that triggers constant propagation within a function? Is it important that we are using a symbol?
The only real difference is the former is in the toplevel lexical environment, and the latter is in the lexical environment of the function., but they are both immutable constants and should be stack/register allocated, so this is weird.
@Mark Kittisopikul https://discourse.julialang.org/t/unexpected-type-instability-with-getproperty-but-not-setproperty/26975/15
Yes, I've been reading that. I think I see what Kristoffer is saying now after reading it for the 5th time though.
julia> @macroexpand @code_warntype (a = 5, b = 5.0).a
:(InteractiveUtils.code_warntype(getproperty, (Base.typesof)((a = 5, b = 5.0), :a)))
I am trying to make sense of it still. It seems to be just how @code_warntype is implemented. Maybe try looking at the lowered or native code of both?
Cthulhu might come in really handy here
In the first case, we are just asking for result of giving getproperty
a NamedTuple
and a Symbol
. There is no constant :a
to propagate.
julia> InteractiveUtils.code_warntype(getproperty, Tuple{NamedTuple{(:a, :b), Tuple{Int64, Float64}}, Symbol})
MethodInstance for getproperty(::NamedTuple{(:a, :b), Tuple{Int64, Float64}}, ::Symbol)
from getproperty(x, f::Symbol) in Base at Base.jl:42
Arguments
#self#::Core.Const(getproperty)
x::NamedTuple{(:a, :b), Tuple{Int64, Float64}}
f::Symbol
Body::Union{Float64, Int64}
1 ─ nothing
│ %2 = Base.getfield(x, f)::Union{Float64, Int64}
└── return %2
Aye. This seems to be another case of "prefer functions over toplevel data"
Yeah, put the second into a function but with .a
as a constant, and it'll propagate just fine.
Mark Kittisopikul has marked this topic as resolved.
I learned something here. I still want to explore the bounds of how this works, but I think I understand more than when I did several hours ago.
"don't benchmark in global scope" really should be "don't inspect compiler optimizations in global scope" :thinking: if it requires constant prop, you need a compiled context to propagate in, which only happens in functions
Last updated: Nov 06 2024 at 04:40 UTC