Skip to content

Commit caa2f7d

Browse files
authored
infer more completely everything that the optimizer/codegen requires (JuliaLang#56565)
Inlining wants to know information about every isa_compileable_sig method as well as everything it might consider inlining (which is almost the same thing). So even if inference could bail on computing the type since it already reached the maximum fixed point, it should keep going to get that information. This now uses two loops here now: one to compute the inference types information, then a second loop go back and get coverage of all of the compileable targets (unless that particular target is predicted to be inlined or dropped later). (system image size contribution seems to be fairly negligible)
1 parent 5ec3215 commit caa2f7d

File tree

4 files changed

+94
-79
lines changed

4 files changed

+94
-79
lines changed

Compiler/src/abstractinterpretation.jl

Lines changed: 84 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,24 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
5151
end
5252

5353
(; valid_worlds, applicable, info) = matches
54-
update_valid_age!(sv, valid_worlds)
54+
update_valid_age!(sv, valid_worlds) # need to record the negative world now, since even if we don't generate any useful information, inlining might want to add an invoke edge and it won't have this information anymore
55+
if bail_out_toplevel_call(interp, sv)
56+
napplicable = length(applicable)
57+
for i = 1:napplicable
58+
sig = applicable[i].match.spec_types
59+
if !isdispatchtuple(sig)
60+
# only infer fully concrete call sites in top-level expressions (ignoring even isa_compileable_sig matches)
61+
add_remark!(interp, sv, "Refusing to infer non-concrete call site in top-level expression")
62+
return Future(CallMeta(Any, Any, Effects(), NoCallInfo()))
63+
end
64+
end
65+
end
5566

5667
# final result
5768
gfresult = Future{CallMeta}()
5869
# intermediate work for computing gfresult
5970
rettype = exctype = Bottom
6071
conditionals = nothing # keeps refinement information of call argument types when the return type is boolean
61-
seenall = true
6272
const_results = nothing # or const_results::Vector{Union{Nothing,ConstResult}} if any const results are available
6373
fargs = arginfo.fargs
6474
all_effects = EFFECTS_TOTAL
@@ -69,16 +79,14 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
6979
f = Core.Box(f)
7080
atype = Core.Box(atype)
7181
function infercalls(interp, sv)
72-
napplicable = length(applicable)
73-
multiple_matches = napplicable > 1
82+
local napplicable = length(applicable)
83+
local multiple_matches = napplicable > 1
7484
while i <= napplicable
7585
(; match, edges, edge_idx) = applicable[i]
7686
method = match.method
7787
sig = match.spec_types
78-
if bail_out_toplevel_call(interp, InferenceLoopState(sig, rettype, all_effects), sv)
79-
# only infer concrete call sites in top-level expressions
80-
add_remark!(interp, sv, "Refusing to infer non-concrete call site in top-level expression")
81-
seenall = false
88+
if bail_out_call(interp, InferenceLoopState(rettype, all_effects), sv)
89+
add_remark!(interp, sv, "Call inference reached maximally imprecise information: bailing on doing more abstract inference.")
8290
break
8391
end
8492
# TODO: this is unmaintained now as it didn't seem to improve things, though it does avoid hard-coding the union split at the higher level,
@@ -162,17 +170,13 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
162170
Any[Bottom for _ in 1:length(argtypes)]
163171
end
164172
for i = 1:length(argtypes)
165-
cnd = conditional_argtype(𝕃ᵢ, this_conditional, sig, argtypes, i)
173+
cnd = conditional_argtype(𝕃ᵢ, this_conditional, match.spec_types, argtypes, i)
166174
conditionals[1][i] = conditionals[1][i] ᵢ cnd.thentype
167175
conditionals[2][i] = conditionals[2][i] ᵢ cnd.elsetype
168176
end
169177
end
170178
edges[edge_idx] = edge
171-
if i < napplicable && bail_out_call(interp, InferenceLoopState(sig, rettype, all_effects), sv)
172-
add_remark!(interp, sv, "Call inference reached maximally imprecise information. Bailing on.")
173-
seenall = false
174-
i = napplicable # break in outer function
175-
end
179+
176180
i += 1
177181
return true
178182
end # function handle1
@@ -184,12 +188,12 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
184188
end
185189
end # while
186190

187-
if const_results !== nothing
188-
@assert napplicable == nmatches(info) == length(const_results)
189-
info = ConstCallInfo(info, const_results)
190-
end
191-
192-
if seenall
191+
seenall = i > napplicable
192+
if seenall # small optimization to skip some work that is already implied
193+
if const_results !== nothing
194+
@assert napplicable == nmatches(info) == length(const_results)
195+
info = ConstCallInfo(info, const_results)
196+
end
193197
if !fully_covering(matches) || any_ambig(matches)
194198
# Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature.
195199
all_effects = Effects(all_effects; nothrow=false)
@@ -198,51 +202,67 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
198202
if sv isa InferenceState && fargs !== nothing
199203
slotrefinements = collect_slot_refinements(𝕃ᵢ, applicable, argtypes, fargs, sv)
200204
end
205+
rettype = from_interprocedural!(interp, rettype, sv, arginfo, conditionals)
206+
if call_result_unused(si) && !(rettype === Bottom)
207+
add_remark!(interp, sv, "Call result type was widened because the return value is unused")
208+
# We're mainly only here because the optimizer might want this code,
209+
# but we ourselves locally don't typically care about it locally
210+
# (beyond checking if it always throws).
211+
# So avoid adding an edge, since we don't want to bother attempting
212+
# to improve our result even if it does change (to always throw),
213+
# and avoid keeping track of a more complex result type.
214+
rettype = Any
215+
end
216+
# if from_interprocedural added any pclimitations to the set inherited from the arguments,
217+
# some of those may be part of our cycles, so those can be deleted now
218+
# TODO: and those might need to be deleted later too if the cycle grows to include them?
219+
if isa(sv, InferenceState)
220+
# TODO (#48913) implement a proper recursion handling for irinterp:
221+
# This works just because currently the `:terminate` condition guarantees that
222+
# irinterp doesn't fail into unresolved cycles, but it's not a good solution.
223+
# We should revisit this once we have a better story for handling cycles in irinterp.
224+
if !isempty(sv.pclimitations) # remove self, if present
225+
delete!(sv.pclimitations, sv)
226+
for caller in callers_in_cycle(sv)
227+
delete!(sv.pclimitations, caller)
228+
end
229+
end
230+
end
201231
else
202232
# there is unanalyzed candidate, widen type and effects to the top
203233
rettype = exctype = Any
204234
all_effects = Effects()
235+
const_results = nothing
205236
end
206237

207-
rettype = from_interprocedural!(interp, rettype, sv, arginfo, conditionals)
208-
209238
# Also considering inferring the compilation signature for this method, so
210-
# it is available to the compiler, unless it should not end up needing it (for an invoke).
211-
if (isa(sv, InferenceState) && infer_compilation_signature(interp) &&
212-
(seenall && 1 == napplicable) && (!is_removable_if_unused(all_effects) || !call_result_unused(si)))
213-
(; match) = applicable[1]
214-
method = match.method
215-
sig = match.spec_types
216-
mi = specialize_method(match; preexisting=true)
217-
if mi === nothing || !const_prop_methodinstance_heuristic(interp, mi, arginfo, sv)
218-
csig = get_compileable_sig(method, sig, match.sparams)
219-
if csig !== nothing && csig !== sig
220-
abstract_call_method(interp, method, csig, match.sparams, multiple_matches, StmtInfo(false), sv)::Future
221-
end
222-
end
223-
end
224-
225-
if call_result_unused(si) && !(rettype === Bottom)
226-
add_remark!(interp, sv, "Call result type was widened because the return value is unused")
227-
# We're mainly only here because the optimizer might want this code,
228-
# but we ourselves locally don't typically care about it locally
229-
# (beyond checking if it always throws).
230-
# So avoid adding an edge, since we don't want to bother attempting
231-
# to improve our result even if it does change (to always throw),
232-
# and avoid keeping track of a more complex result type.
233-
rettype = Any
234-
end
235-
if isa(sv, InferenceState)
236-
# TODO (#48913) implement a proper recursion handling for irinterp:
237-
# This works just because currently the `:terminate` condition guarantees that
238-
# irinterp doesn't fail into unresolved cycles, but it's not a good solution.
239-
# We should revisit this once we have a better story for handling cycles in irinterp.
240-
if !isempty(sv.pclimitations) # remove self, if present
241-
delete!(sv.pclimitations, sv)
242-
for caller in callers_in_cycle(sv)
243-
delete!(sv.pclimitations, caller)
239+
# it is available to the compiler in case it ends up needing it for the invoke.
240+
if isa(sv, InferenceState) && infer_compilation_signature(interp) && (!is_removable_if_unused(all_effects) || !call_result_unused(si))
241+
i = 1
242+
function infercalls2(interp, sv)
243+
local napplicable = length(applicable)
244+
local multiple_matches = napplicable > 1
245+
while i <= napplicable
246+
(; match, edges, edge_idx) = applicable[i]
247+
i += 1
248+
method = match.method
249+
sig = match.spec_types
250+
mi = specialize_method(match; preexisting=true)
251+
if mi === nothing || !const_prop_methodinstance_heuristic(interp, mi, arginfo, sv)
252+
csig = get_compileable_sig(method, sig, match.sparams)
253+
if csig !== nothing && (!seenall || csig !== sig) # corresponds to whether the first look already looked at this, so repeating abstract_call_method is not useful
254+
sp_ = ccall(:jl_type_intersection_with_env, Any, (Any, Any), csig, method.sig)::SimpleVector
255+
if match.sparams === sp_[2]
256+
mresult = abstract_call_method(interp, method, csig, match.sparams, multiple_matches, StmtInfo(false), sv)::Future
257+
isready(mresult) || return false # wait for mresult Future to resolve off the callstack before continuing
258+
end
259+
end
260+
end
244261
end
262+
return true
245263
end
264+
# start making progress on the first call
265+
infercalls2(interp, sv) || push!(sv.tasks, infercalls2)
246266
end
247267

248268
gfresult[] = CallMeta(rettype, exctype, all_effects, info, slotrefinements)
@@ -1787,6 +1807,14 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si::
17871807
i = 1
17881808
while i <= length(ctypes)
17891809
ct = ctypes[i]
1810+
if bail_out_apply(interp, InferenceLoopState(res, all_effects), sv)
1811+
add_remark!(interp, sv, "_apply_iterate inference reached maximally imprecise information: bailing on analysis of more methods.")
1812+
# there is unanalyzed candidate, widen type and effects to the top
1813+
let retinfo = NoCallInfo() # NOTE this is necessary to prevent the inlining processing
1814+
applyresult[] = CallMeta(Any, Any, Effects(), retinfo)
1815+
return true
1816+
end
1817+
end
17901818
lct = length(ct)
17911819
# truncate argument list at the first Vararg
17921820
for k = 1:lct-1
@@ -1808,14 +1836,6 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si::
18081836
res = tmerge(typeinf_lattice(interp), res, rt)
18091837
exctype = tmerge(typeinf_lattice(interp), exctype, exct)
18101838
all_effects = merge_effects(all_effects, effects)
1811-
if i < length(ctypes) && bail_out_apply(interp, InferenceLoopState(ctypes[i], res, all_effects), sv)
1812-
add_remark!(interp, sv, "_apply_iterate inference reached maximally imprecise information. Bailing on.")
1813-
# there is unanalyzed candidate, widen type and effects to the top
1814-
let retinfo = NoCallInfo() # NOTE this is necessary to prevent the inlining processing
1815-
applyresult[] = CallMeta(Any, Any, Effects(), retinfo)
1816-
return true
1817-
end
1818-
end
18191839
end
18201840
i += 1
18211841
end

Compiler/src/inferencestate.jl

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,17 +1032,15 @@ decode_statement_effects_override(sv::AbsIntState) =
10321032
decode_statement_effects_override(get_curr_ssaflag(sv))
10331033

10341034
struct InferenceLoopState
1035-
sig
10361035
rt
10371036
effects::Effects
1038-
function InferenceLoopState(@nospecialize(sig), @nospecialize(rt), effects::Effects)
1039-
new(sig, rt, effects)
1037+
function InferenceLoopState(@nospecialize(rt), effects::Effects)
1038+
new(rt, effects)
10401039
end
10411040
end
10421041

1043-
bail_out_toplevel_call(::AbstractInterpreter, state::InferenceLoopState, sv::InferenceState) =
1044-
sv.restrict_abstract_call_sites && !isdispatchtuple(state.sig)
1045-
bail_out_toplevel_call(::AbstractInterpreter, ::InferenceLoopState, ::IRInterpretationState) = false
1042+
bail_out_toplevel_call(::AbstractInterpreter, sv::InferenceState) = sv.restrict_abstract_call_sites
1043+
bail_out_toplevel_call(::AbstractInterpreter, ::IRInterpretationState) = false
10461044

10471045
bail_out_call(::AbstractInterpreter, state::InferenceLoopState, ::InferenceState) =
10481046
state.rt === Any && !is_foldable(state.effects)

Compiler/test/AbstractInterpreter.jl

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,15 @@ end |> !Core.Compiler.is_nonoverlayed
7070

7171
# account for overlay possibility in unanalyzed matching method
7272
callstrange(::Float64) = strangesin(x)
73-
callstrange(::Nothing) = Core.compilerbarrier(:type, nothing) # trigger inference bail out
73+
callstrange(::Number) = Core.compilerbarrier(:type, nothing) # trigger inference bail out
74+
callstrange(::Any) = 1.0
7475
callstrange_entry(x) = callstrange(x) # needs to be defined here because of world age
7576
let interp = MTOverlayInterp(Set{Any}())
7677
matches = Core.Compiler.findall(Tuple{typeof(callstrange),Any}, Core.Compiler.method_table(interp))
7778
@test matches !== nothing
78-
@test Core.Compiler.length(matches) == 2
79-
if Core.Compiler.getindex(matches, 1).method == which(callstrange, (Nothing,))
80-
@test Base.infer_effects(callstrange_entry, (Any,); interp) |> !Core.Compiler.is_nonoverlayed
81-
@test "Call inference reached maximally imprecise information. Bailing on." in interp.meta
82-
else
83-
@warn "`nonoverlayed` test for inference bailing out is skipped since the method match sort order is changed."
84-
end
79+
@test Core.Compiler.length(matches) == 3
80+
@test Base.infer_effects(callstrange_entry, (Any,); interp) |> !Core.Compiler.is_nonoverlayed
81+
@test "Call inference reached maximally imprecise information: bailing on doing more abstract inference." in interp.meta
8582
end
8683

8784
# but it should never apply for the native compilation

Compiler/test/inference.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4114,7 +4114,7 @@ callsig_backprop_any(::Any) = nothing
41144114
callsig_backprop_lhs(::Int) = nothing
41154115
callsig_backprop_bailout(::Val{0}) = 0
41164116
callsig_backprop_bailout(::Val{1}) = undefvar # undefvar::Any triggers `bail_out_call`
4117-
callsig_backprop_bailout(::Val{2}) = 2
4117+
callsig_backprop_bailout(::Val) = 2
41184118
callsig_backprop_addinteger(a::Integer, b::Integer) = a + b # results in too many matching methods and triggers `bail_out_call`)
41194119
@test Base.infer_return_type(callsig_backprop_addinteger) == Any
41204120
let effects = Base.infer_effects(callsig_backprop_addinteger)

0 commit comments

Comments
 (0)