Skip to content

Commit 481d3cf

Browse files
committed
inference: enable constant propagation for union-split signatures
The inference precision of certain functions really relies on constant propagation, but currently constant prop' won't happen when a call signature is union split and so sometimes inference ends up looser return type: e.g. ```julia julia> Base.return_types((Union{Tuple{Int,Nothing},Tuple{Int,Missing}},)) do t a, b = t a # I expected a::Int, but a::Union{Missing,Nothing,Int} end |> first Union{Missing, Nothing, Int64} ``` This PR: - enables constant prop' for union signatures (with "non-Bottom" results) - adds special cases for some functions (see `force_constant_prop'`) so that we keep inferring on them even after the return type from non-constant analysis has grown to `Any`, since constant analysis can improve the result later on The added test cases will should showcase the cases where the inference result could be improved by that. --- Here is a sample benchmark of the impact of this PR on latency, from which I guess this PR is acceptable ? > build time: master (26a721b) ```bash Sysimage built. Summary: Total ─────── 57.320450 seconds Base: ─────── 24.371516 seconds 42.518% Stdlibs: ──── 32.947400 seconds 57.4793% JULIA usr/lib/julia/sys-o.a Generating REPL precompile statements... 30/30 Executing precompile statements... 1379/1379 Precompilation complete. Summary: Total ─────── 101.325866 seconds Generation ── 74.901320 seconds 73.9212% Execution ─── 26.424546 seconds 26.0788% LINK usr/lib/julia/sys.dylib ``` > build time: this PR ```bash Sysimage built. Summary: Total ─────── 58.387562 seconds Base: ─────── 24.403206 seconds 41.7952% Stdlibs: ──── 33.982657 seconds 58.2019% JULIA usr/lib/julia/sys-o.a Generating REPL precompile statements... 30/30 Executing precompile statements... 1362/1362 Precompilation complete. Summary: Total ─────── 99.659102 seconds Generation ── 73.983763 seconds 74.2368% Execution ─── 25.675339 seconds 25.7632% LINK usr/lib/julia/sys.dylib ``` > first time to plot: master (26a721b) ```julia julia> using Plots; @time plot(rand(10,3)) 3.614168 seconds (5.47 M allocations: 324.564 MiB, 5.73% gc time, 53.02% compilation time) ``` > first time to plot: this PR ```julia julia> using Plots; @time plot(rand(10,3)) 3.557919 seconds (5.53 M allocations: 328.812 MiB, 2.89% gc time, 51.94% compilation time) ``` --- NOTE: this PR is an alternative for JuliaLang#39296
1 parent 26a721b commit 481d3cf

File tree

2 files changed

+106
-16
lines changed

2 files changed

+106
-16
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
9797
napplicable = length(applicable)
9898
rettype = Bottom
9999
edgecycle = false
100-
edges = Any[]
101-
nonbot = 0 # the index of the only non-Bottom inference result if > 0
100+
edges = MethodInstance[]
101+
nonbots = Int[] # the indexes of non-Bottom inference results (will be re-analyzed with constant arguments)
102102
seen = 0 # number of signatures actually inferred
103103
istoplevel = sv.linfo.def isa Module
104104
multiple_matches = napplicable > 1
@@ -111,6 +111,8 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
111111
end
112112
end
113113

114+
keep_inference = force_constant_prop(interp, f, argtypes)
115+
114116
for i in 1:napplicable
115117
match = applicable[i]::MethodMatch
116118
method = match.method
@@ -122,8 +124,8 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
122124
break
123125
end
124126
sigtuple = unwrap_unionall(sig)::DataType
125-
splitunions = false
126127
this_rt = Bottom
128+
splitunions = false
127129
# TODO: splitunions = 1 < unionsplitcost(sigtuple.parameters) * napplicable <= InferenceParams(interp).MAX_UNION_SPLITTING
128130
# currently this triggers a bug in inference recursion detection
129131
if splitunions
@@ -135,7 +137,9 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
135137
end
136138
edgecycle |= edgecycle1::Bool
137139
this_rt = tmerge(this_rt, rt)
138-
this_rt === Any && break
140+
if !(this_rt !== Any || keep_inference)
141+
break
142+
end
139143
end
140144
else
141145
this_rt, edgecycle1, edge = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv)
@@ -145,29 +149,32 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
145149
end
146150
end
147151
if this_rt !== Bottom
148-
if nonbot === 0
149-
nonbot = i
150-
else
151-
nonbot = -1
152-
end
152+
push!(nonbots, i)
153153
end
154154
seen += 1
155155
rettype = tmerge(rettype, this_rt)
156-
rettype === Any && break
156+
if !(rettype !== Any || keep_inference)
157+
break
158+
end
157159
end
158-
# try constant propagation if only 1 method is inferred to non-Bottom
160+
# try constant propagation for non-Bottom inference results
159161
# this is in preparation for inlining, or improving the return result
160162
is_unused = call_result_unused(sv)
161-
if nonbot > 0 && seen == napplicable && (!edgecycle || !is_unused) &&
163+
if !isempty(nonbots) && seen == napplicable && (!edgecycle || !is_unused) &&
162164
is_improvable(rettype) && InferenceParams(interp).ipo_constant_propagation
163165
# if there's a possibility we could constant-propagate a better result
164166
# (hopefully without doing too much work), try to do that now
165167
# TODO: it feels like this could be better integrated into abstract_call_method / typeinf_edge
166-
const_rettype = abstract_call_method_with_const_args(interp, rettype, f, argtypes, applicable[nonbot]::MethodMatch, sv, edgecycle)
167-
if const_rettype rettype
168-
# use the better result, if it's a refinement of rettype
169-
rettype = const_rettype
168+
const_rettype = Bottom
169+
for nonbot in nonbots
170+
this_rt = abstract_call_method_with_const_args(interp, rettype, f, argtypes, applicable[nonbot]::MethodMatch, sv, edgecycle)
171+
const_rettype = tmerge(const_rettype, this_rt)
172+
if !(const_rettype !== rettype && const_rettype rettype)
173+
const_rettype = rettype
174+
break
175+
end
170176
end
177+
rettype = const_rettype
171178
end
172179
if is_unused && !(rettype === Bottom)
173180
add_remark!(interp, sv, "Call result type was widened because the return value is unused")
@@ -195,6 +202,21 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
195202
return CallMeta(rettype, info)
196203
end
197204

205+
# some functions heavily rely on constant propagation to get precise return type,
206+
# so we better to keep inference on them even after the return type has grown to `Any`
207+
function force_constant_prop(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any})
208+
istopfunction(f, :getproperty) && return true
209+
istopfunction(f, :setproperty!) && return true
210+
la = length(argtypes)
211+
# tuple indexing
212+
if la 3 &&
213+
(a3 = argtypes[3]; isa(a3, Const) && isa(a3.val, Int)) &&
214+
(a2 = argtypes[2]; a2 Tuple && isa(a2, Union)) &&
215+
istopfunction(f, :getindex)
216+
return true
217+
end
218+
return false
219+
end
198220

199221
function const_prop_profitable(@nospecialize(arg))
200222
# have new information from argtypes that wasn't available from the signature

test/compiler/inference.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ isdispatchelem(@nospecialize x) = !isa(x, Type) || Core.Compiler.isdispatchelem(
77
using Random, Core.IR
88
using InteractiveUtils: code_llvm
99

10+
macro evaltoplevel(ex)
11+
m = Core.eval(__module__, :(module $(gensym()) end))::Module
12+
return QuoteNode(Core.eval(m, ex))
13+
end
14+
1015
f39082(x::Vararg{T}) where {T <: Number} = x[1]
1116
let ast = only(code_typed(f39082, Tuple{Vararg{Rational}}))[1]
1217
@test ast.slottypes == Any[Const(f39082), Tuple{Vararg{Rational}}]
@@ -3008,3 +3013,66 @@ f38888() = S38888(Base.inferencebarrier(3))
30083013
@test f38888() isa S38888
30093014
g38888() = S38888(Base.inferencebarrier(3), nothing)
30103015
@test g38888() isa S38888
3016+
3017+
@testset "constant prop' for union split signature" begin
3018+
# indexing into tuples really relies on constant prop', and we will get looser result
3019+
# (`Union{Int,String,Char}`) if constant prop' doesn't happen for splitunion signatures
3020+
tt = (Union{Tuple{Int,String},Tuple{Int,Char}},)
3021+
3022+
# `getindex`
3023+
@test Base.return_types(tt) do t
3024+
getindex(t, 1)
3025+
end == Any[Int]
3026+
@test Base.return_types(tt) do t
3027+
getindex(t, 2)
3028+
end == Any[Union{String,Char}]
3029+
3030+
# `indexed_iterate`
3031+
@test Base.return_types(tt) do t
3032+
a, b = t
3033+
a
3034+
end == Any[Int]
3035+
@test Base.return_types(tt) do t
3036+
a, b = t
3037+
b
3038+
end == Any[Union{String,Char}]
3039+
3040+
@test @evaltoplevel begin
3041+
struct F32
3042+
val::Float32
3043+
_v::Int
3044+
end
3045+
struct F64
3046+
val::Float64
3047+
_v::Int
3048+
end
3049+
Base.return_types((Union{F32,F64},)) do f
3050+
f.val
3051+
end == Any[Union{Float32,Float64}]
3052+
end
3053+
3054+
# for some of functions, we should force constant propagation by keeping inference
3055+
# even after the return type of non-constant analysis has grown to `Any` to get precise
3056+
# return type of some functions
3057+
@testset "force constant prop'" begin
3058+
# `getproperty`
3059+
@test @evaltoplevel begin
3060+
struct F32
3061+
val::Float32
3062+
_v
3063+
end
3064+
struct F64
3065+
val::Float64
3066+
_v
3067+
end
3068+
Base.return_types((Union{F32,F64},)) do f
3069+
f.val
3070+
end == Any[Union{Float32,Float64}]
3071+
end
3072+
3073+
# `getindex(::Tuple, ::Const(::Int))`
3074+
@test Base.return_types((Union{Tuple{Nothing,Any,Any},Tuple{Nothing,Any}},)) do t
3075+
getindex(t, 1)
3076+
end == Any[Nothing]
3077+
end
3078+
end

0 commit comments

Comments
 (0)