-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inline statically known method errors. #54972
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally dislike optimizing codepaths that aren't performance critical, as it bloats the code, slows the compiler, and introduces more possibilities for subtle discrepancies in what error is generated and thrown
I share that concern, but in this case the transformation is pretty minimal I think. It turns: Expr(:call, foo, args...)::Union{} into: %world = Expr(:foreigncall, :(:jl_get_tls_world_age), UInt64, svec(), 0, :(:ccall))::UInt64
%argtuple = Expr(:call, tuple, args...)
%err = Expr(:new, MethodError, farg, %argtuple, %world)
Expr(:call, throw, %err) (note that since #54537 the Those 2-3 extra instructions don't seem too bad to me. |
else | ||
for (thisfullmatch, mt) in zip(matches.fullmatches, matches.mts) | ||
matches::UnionSplitMethodMatches |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
matches::UnionSplitMethodMatches |
This change does not seem to bloat up the sysimg, but I have the same concern and would prefer to make it an optional feature enabled by a new optimization parameter. |
We can do that, but it puts a tool like AllocCheck in an awkward position since it would now want to run with custom inference for precision purposes - but that can now diverge from the codegen/analysis for the standard inference settings (even if the "dynamic" dispatch itself is harmless in both cases) so That guarantee is already weak (since inference can be noticeably stateful in, e.g., recursive cycles), but I'd prefer not to make it worse if we can avoid it |
To avoid IR bloat, would it preferable to lower this to a new apply-like builtin instead? Something like |
This seems like one possible approach, but adding it solely for this purpose feels a bit hesitant. This is because inlining the code to raise a |
The issue is that a call that does something and then errors vs something that immediately errors is different IMO. i.e I have a dispatch to something that does a while true loop and the only way it exits is by erroring. |
I see. But if we assume that allocations on throw code path are not performance-critical, wouldn't it be okay for AllocCheck to ignore such calls? |
AllocCheck also supports analysis including throw paths, it's just not turned on by default. |
@@ -1233,6 +1233,7 @@ end | |||
@testset "throw=true" begin | |||
tasks, event = create_tasks() | |||
push!(tasks, Threads.@spawn error("Error")) | |||
wait(tasks[end]; throw=false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a race condition in the test that started failing after these changes, for some reason
66296c0
to
98f272f
Compare
8136192
to
443890b
Compare
Alright this now turns the call into: Core.throw_methoderror(f, args...)
unreachable which should hopefully resolve the IR bloat concerns |
This allows us to simulate/mark calls that are known-to-fail. Required for #54972
dc173fb
to
b5e10c8
Compare
LGTM modulo the comment posted above. |
75f9d08
to
e5beb8a
Compare
Should we run benchmarks/pkgeval? |
@nanosoldier PkgEval is probably unnecessary - this shouldn't have any observable semantic differences |
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. |
This allows us to simulate/mark calls that are known-to-fail. Required for #54972
This allows us to simulate/mark calls that are known-to-fail. Required for #54972
base/compiler/types.jl
Outdated
|
||
nsplit_impl(::CallInfo) = nothing | ||
getsplit_impl(::CallInfo, ::Int) = error("unexpected call into `getsplit`") | ||
getresult_impl(::CallInfo, ::Int) = nothing | ||
add_uncovered_edges_impl(edges::Vector{Any}, info::CallInfo, @nospecialize(atype)) = nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this default implementation, for any arbitrary custom CallInfo
, as long as it correctly implements getsplit_impl
, the method error case inlining enabled by this PR will occur. However, if that CallInfo
does not implement add_uncovered_edges_impl
, there could theoretically be cases where the necessary edges are not added. In the version I proposed, the getmatchinfo(::CallInfo, ::Int)
interface is enforced to be implemented for all CallInfo
s, so I think such interface requirement errors are easy to detect. That said, this is a very rare case, so I think it’s just fine to revisit it later when there’s more time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree that it'd be better to make this error if unimplemented (TBH I don't know why we provide a default implementation for any of these)
The getmatchinfo(::CallInfo, ::Int)
isn't quite right, because it treats uncovered edges as being associated with split indices, but they are the edge(s) not covered by the split cases (e.g. you might have 10 split cases, and then an uncovered atype
that needs an edge to 2 method tables)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
getmatchinfo(::CallInfo, ::Int)
isn't quite right, because it treats uncovered edges as being associated with split indices, but they are the edge(s) not covered by the split cases (e.g. you might have 10 split cases, and then an uncoveredatype
that needs an edge to 2 method tables)
Right... So maybe getuncoveredmt(::CallInfo) -> iterator of method tables with !fullmatch
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why we provide a default implementation for any of these
Since they are optional. If CallInfo
implements nsplit
, then the inlining for the call is enabled and the CallInfo
is required to implement getsplit
. And getresult
is optional, and if it's implemented, then the inliner may use a result from extended lattice inference (a.k.a. const-prop and semi-concrete interpretation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's helpful - sounds like this one should be:
add_uncovered_edges_impl(edges::Vector{Any}, info::CallInfo, @nospecialize(atype)) = error("unexpected call into add_uncovered_edges!")
since we also only do this if you implement nsplit
That said, getuncoveredmt
would also be OK w/ me - the API shape seems orthogonal to the optionality, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intend for us to eliminate all this backwards-designed tracking code for better correctness, as soon as someone can help finish #54894
e5beb8a
to
c795008
Compare
This replaces the Expr(:call, ...) with a call to Core.throw_methoderror Co-authored-by: Cody Tapscott <topolarity@tapscott.me>
We need to add MT edges for any (handled, but) not fully-covered edges in the union-split of a `CallInfo`
c795008
to
f5223c2
Compare
matches::Vector{MethodMatchInfo} | ||
matches::Vector{MethodLookupResult} | ||
mts::Vector{MethodTable} | ||
fullmatches::Vector{Bool} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be okay to revert back to storing Vector{MethodMatchInfo}
like before? The current design, where matches
/mts
/fullmatches
can have different lengths, feels a bit tricky to handle. I understand it's meant to avoid adding duplicate edges to the same mt
in add_uncovered_edges_impl
, but in practice, adding duplicate edges isn't really an issue. From a complexity standpoint, I think the previous approach is preferable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you feel about:
matches::Vector{MethodMatchInfo} | |
matches::Vector{MethodLookupResult} | |
mts::Vector{MethodTable} | |
fullmatches::Vector{Bool} | |
matches::Vector{MethodLookupResult} | |
mt_edges::Vector{@NamedTuple{mt::MethodTable, fullmatch::Bool}} |
or similar?
That's more consistent with what UnionSplitMethodMatches
carried before, which always had these edges de-duplicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, the previous design of UnionSplitMethodMatches
also avoided adding duplicated edges. But I still feel that it's better for UnionSplitInfo
or UnionSplitMethodMatches
to have a simpler data structure. We can avoid duplication when adding edges.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, feel free to open a PR
This replaces the
Expr(:call, ...)
with a call of a new builtinCore.throw_methoderror
This is useful because it makes very clear if something is a static method error or a plain dynamic dispatch that always errors.
Tools such as AllocCheck or juliac can notice that this is not a genuine dynamic dispatch, and prevent it from becoming a false positive compile-time error.
Dependent on #55705