Skip to content
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

optimizer: allow EA-powered finalizer inlining #55954

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ function refine_effects!(interp::AbstractInterpreter, opt::OptimizationState, sv
if !is_effect_free(sv.result.ipo_effects) && sv.all_effect_free && !isempty(sv.ea_analysis_pending)
ir = sv.ir
nargs = Int(opt.src.nargs)
estate = EscapeAnalysis.analyze_escapes(ir, nargs, optimizer_lattice(interp), GetNativeEscapeCache(interp))
estate = EscapeAnalysis.analyze_escapes(ir, nargs, optimizer_lattice(interp), get_escape_cache(interp))
argescapes = EscapeAnalysis.ArgEscapeCache(estate)
stack_analysis_result!(sv.result, argescapes)
validate_mutable_arg_escapes!(estate, sv)
Expand Down
59 changes: 41 additions & 18 deletions base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ._TOP_MOD: ==, getindex, setindex!
using Core: MethodMatch, SimpleVector, ifelse, sizeof
using Core.IR
using ._TOP_MOD: # Base definitions
@__MODULE__, @assert, @eval, @goto, @inbounds, @inline, @label, @noinline,
@__MODULE__, @assert, @eval, @goto, @inbounds, @inline, @label, @noinline, @show,
@nospecialize, @specialize, BitSet, Callable, Csize_t, IdDict, IdSet, UnitRange, Vector,
copy, delete!, empty!, enumerate, error, first, get, get!, haskey, in, isassigned,
isempty, ismutabletype, keys, last, length, max, min, missing, pop!, push!, pushfirst!,
Expand Down Expand Up @@ -657,11 +657,13 @@ function analyze_escapes(ir::IRCode, nargs::Int, 𝕃ₒ::AbstractLattice, get_e
# `escape_exception!` conservatively propagates `AllEscape` anyway,
# and so escape information imposed on `:the_exception` isn't computed
continue
elseif head === :gc_preserve_begin
# GC preserve is handled by `escape_gc_preserve!`
elseif head === :gc_preserve_end
escape_gc_preserve!(astate, pc, stmt.args)
elseif head === :static_parameter || # this exists statically, not interested in its escape
head === :copyast || # XXX can this account for some escapes?
head === :isdefined || # just returns `Bool`, nothing accounts for any escapes
head === :gc_preserve_begin || # `GC.@preserve` expressions themselves won't be used anywhere
head === :gc_preserve_end # `GC.@preserve` expressions themselves won't be used anywhere
head === :copyast || # XXX escape something?
head === :isdefined # just returns `Bool`, nothing accounts for any escapes
continue
else
add_conservative_changes!(astate, pc, stmt.args)
Expand Down Expand Up @@ -1064,17 +1066,27 @@ end
function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any})
mi = first(args)::MethodInstance
first_idx, last_idx = 2, length(args)
add_liveness_changes!(astate, pc, args, first_idx, last_idx)
# TODO inspect `astate.ir.stmts[pc][:info]` and use const-prop'ed `InferenceResult` if available
cache = astate.get_escape_cache(mi)
ret = SSAValue(pc)
if cache isa Bool
if cache
return nothing # guaranteed to have no escape
# This method call is very simple and has good effects, so there's no need to
# escape its arguments. However, since the arguments might be returned, we need
# to consider the possibility of aliasing between them and the return value.
for argidx = first_idx:last_idx
arg = args[argidx]
if !is_mutation_free_argtype(argextype(arg, astate.ir))
add_alias_change!(astate, ret, arg)
end
end
return nothing
else
return add_conservative_changes!(astate, pc, args, 2)
end
end
cache = cache::ArgEscapeCache
ret = SSAValue(pc)
retinfo = astate.estate[ret] # escape information imposed on the call statement
method = mi.def::Method
nargs = Int(method.nargs)
Expand Down Expand Up @@ -1162,6 +1174,17 @@ function escape_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any})
end
end

function escape_gc_preserve!(astate::AnalysisState, pc::Int, args::Vector{Any})
@assert length(args) == 1 "invalid :gc_preserve_end"
val = args[1]
@assert val isa SSAValue "invalid :gc_preserve_end"
beginstmt = astate.ir[val][:stmt]
@assert isexpr(beginstmt, :gc_preserve_begin) "invalid :gc_preserve_end"
beginargs = beginstmt.args
# COMBAK we might need to add liveness for all statements from `:gc_preserve_begin` to `:gc_preserve_end`
add_liveness_changes!(astate, pc, beginargs)
end

normalize(@nospecialize x) = isa(x, QuoteNode) ? x.value : x

function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any})
Expand All @@ -1187,20 +1210,12 @@ function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any})
if result === missing
# if this call hasn't been handled by any of pre-defined handlers, escape it conservatively
add_conservative_changes!(astate, pc, args)
return
elseif result === true
add_liveness_changes!(astate, pc, args, 2)
return # ThrownEscape is already checked
elseif is_nothrow(astate.ir, pc)
add_liveness_changes!(astate, pc, args, 2)
else
# we escape statements with the `ThrownEscape` property using the effect-freeness
# computed by `stmt_effect_flags` invoked within inlining
# TODO throwness ≠ "effect-free-ness"
if is_nothrow(astate.ir, pc)
add_liveness_changes!(astate, pc, args, 2)
else
add_fallback_changes!(astate, pc, args, 2)
end
return
add_fallback_changes!(astate, pc, args, 2)
end
end

Expand Down Expand Up @@ -1528,4 +1543,12 @@ function escape_array_copy!(astate::AnalysisState, pc::Int, args::Vector{Any})
add_liveness_changes!(astate, pc, args, 6)
end

function escape_builtin!(::typeof(Core.finalizer), astate::AnalysisState, pc::Int, args::Vector{Any})
if length(args) ≥ 3
obj = args[3]
add_liveness_change!(astate, obj, pc) # TODO setup a proper FinalizerEscape?
end
return false
end

end # baremodule EscapeAnalysis
86 changes: 60 additions & 26 deletions base/compiler/ssair/passes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,13 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing)
# Inlining performs legality checks on the finalizer to determine
# whether or not we may inline it. If so, it appends extra arguments
# at the end of the intrinsic. Detect that here.
length(stmt.args) == 5 || continue
if length(stmt.args) == 4 && stmt.args[4] === nothing
# constant case
elseif length(stmt.args) == 5 && stmt.args[4] isa Bool && stmt.args[5] isa MethodInstance
# inlining case
else
continue
end
end
is_finalizer = true
elseif isexpr(stmt, :foreigncall)
Expand Down Expand Up @@ -1685,18 +1691,21 @@ end
function sroa_mutables!(ir::IRCode, defuses::IdDict{Int,Tuple{SPCSet,SSADefUse}}, used_ssas::Vector{Int}, lazydomtree::LazyDomtree, inlining::Union{Nothing,InliningState})
𝕃ₒ = inlining === nothing ? SimpleInferenceLattice.instance : optimizer_lattice(inlining.interp)
lazypostdomtree = LazyPostDomtree(ir)
for (defidx, (intermediaries, defuse)) in defuses
# Check if there are any uses we did not account for. If so, the variable
# escapes and we cannot eliminate the allocation. This works, because we're guaranteed
# not to include any intermediaries that have dead uses. As a result, missing uses will only ever
# show up in the nuses_total count.
nleaves = length(defuse.uses) + length(defuse.defs)
nuses = 0
for iidx in intermediaries
nuses += used_ssas[iidx]
function find_finalizer_useidx(defuse::SSADefUse)
finalizer_useidx = nothing
for (useidx, use) in enumerate(defuse.uses)
if use.kind === :finalizer
# For now: Only allow one finalizer per allocation
finalizer_useidx !== nothing && return false
finalizer_useidx = useidx
end
end
nuses_total = used_ssas[defidx] + nuses - length(intermediaries)
nleaves == nuses_total || continue
if finalizer_useidx === nothing || inlining === nothing
return true
end
return finalizer_useidx
end
for (defidx, (intermediaries, defuse)) in defuses
# Find the type for this allocation
defexpr = ir[SSAValue(defidx)][:stmt]
isexpr(defexpr, :new) || continue
Expand All @@ -1706,22 +1715,47 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int,Tuple{SPCSet,SSADefUse}}
typ = widenconst(typ)
ismutabletype(typ) || continue
typ = typ::DataType
# First check for any finalizer calls
finalizer_useidx = nothing
for (useidx, use) in enumerate(defuse.uses)
if use.kind === :finalizer
# For now: Only allow one finalizer per allocation
finalizer_useidx !== nothing && @goto skip
finalizer_useidx = useidx
end
# Check if there are any uses we did not account for. If so, the variable
# escapes and we cannot eliminate the allocation. This works, because we're guaranteed
# not to include any intermediaries that have dead uses. As a result, missing uses will only ever
# show up in the nuses_total count.
nleaves = length(defuse.uses) + length(defuse.defs)
nuses = 0
for iidx in intermediaries
nuses += used_ssas[iidx]
end
nuses_total = used_ssas[defidx] + nuses - length(intermediaries)
all_eliminated = all_forwarded = true
if finalizer_useidx !== nothing && inlining !== nothing
finalizer_idx = defuse.uses[finalizer_useidx].idx
try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining,
lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info])
deleteat!(defuse.uses, finalizer_useidx)
all_eliminated = all_forwarded = false # can't eliminate `setfield!` calls safely
if nleaves ≠ nuses_total
finalizer_useidx = find_finalizer_useidx(defuse)
if finalizer_useidx isa Int
nargs = length(ir.argtypes) # COMBAK this might need to be `Int(opt.src.nargs)`
estate = EscapeAnalysis.analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache(inlining.interp))
einfo = estate[SSAValue(defidx)]
if EscapeAnalysis.has_no_escape(einfo)
already = BitSet(use.idx for use in defuse.uses)
for idx = einfo.Liveness
if idx ∉ already
push!(defuse.uses, SSAUse(:EALiveness, idx))
end
end
finalizer_idx = defuse.uses[finalizer_useidx].idx
try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining::InliningState,
lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info])
end
end
continue
else
finalizer_useidx = find_finalizer_useidx(defuse)
if finalizer_useidx isa Int
finalizer_idx = defuse.uses[finalizer_useidx].idx
try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining::InliningState,
lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info])
deleteat!(defuse.uses, finalizer_useidx)
all_eliminated = all_forwarded = false # can't eliminate `setfield!` calls safely
elseif !finalizer_useidx
continue
end
end
# Partition defuses by field
fielddefuse = SSADefUse[SSADefUse() for _ = 1:fieldcount(typ)]
Expand Down
2 changes: 2 additions & 0 deletions base/compiler/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ typeinf_lattice(::AbstractInterpreter) = InferenceLattice(BaseInferenceLattice.i
ipo_lattice(::AbstractInterpreter) = InferenceLattice(IPOResultLattice.instance)
optimizer_lattice(::AbstractInterpreter) = SimpleInferenceLattice.instance

get_escape_cache(interp::AbstractInterpreter) = GetNativeEscapeCache(interp)

abstract type CallInfo end

@nospecialize
Expand Down
Loading