Skip to content

Commit b19a7c1

Browse files
authored
optimizer: allow EA-powered finalizer inlining (JuliaLang#55954)
E.g. this allows `finalizer` inlining in the following case: ```julia mutable struct ForeignBuffer{T} const ptr::Ptr{T} end const foreign_buffer_finalized = Ref(false) function foreign_alloc(::Type{T}, length) where T ptr = Libc.malloc(sizeof(T) * length) ptr = Base.unsafe_convert(Ptr{T}, ptr) obj = ForeignBuffer{T}(ptr) return finalizer(obj) do obj Base.@assume_effects :notaskstate :nothrow foreign_buffer_finalized[] = true Libc.free(obj.ptr) end end function f_EA_finalizer(N::Int) workspace = foreign_alloc(Float64, N) GC.@preserve workspace begin (;ptr) = workspace Base.@assume_effects :nothrow @noinline println(devnull, "ptr = ", ptr) end end ``` ```julia julia> @code_typed f_EA_finalizer(42) CodeInfo( 1 ── %1 = Base.mul_int(8, N)::Int64 │ %2 = Core.lshr_int(%1, 63)::Int64 │ %3 = Core.trunc_int(Core.UInt8, %2)::UInt8 │ %4 = Core.eq_int(%3, 0x01)::Bool └─── goto mmtk#3 if not %4 2 ── invoke Core.throw_inexacterror(:convert::Symbol, UInt64::Type, %1::Int64)::Union{} └─── unreachable 3 ── goto mmtk#4 4 ── %9 = Core.bitcast(Core.UInt64, %1)::UInt64 └─── goto mmtk#5 5 ── goto mmtk#6 6 ── goto mmtk#7 7 ── goto mmtk#8 8 ── %14 = $(Expr(:foreigncall, :(:malloc), Ptr{Nothing}, svec(UInt64), 0, :(:ccall), :(%9), :(%9)))::Ptr{Nothing} └─── goto mmtk#9 9 ── %16 = Base.bitcast(Ptr{Float64}, %14)::Ptr{Float64} │ %17 = %new(ForeignBuffer{Float64}, %16)::ForeignBuffer{Float64} └─── goto mmtk#10 10 ─ %19 = $(Expr(:gc_preserve_begin, :(%17))) │ %20 = Base.getfield(%17, :ptr)::Ptr{Float64} │ invoke Main.println(Main.devnull::Base.DevNull, "ptr = "::String, %20::Ptr{Float64})::Nothing │ $(Expr(:gc_preserve_end, :(%19))) │ %23 = Main.foreign_buffer_finalized::Base.RefValue{Bool} │ Base.setfield!(%23, :x, true)::Bool │ %25 = Base.getfield(%17, :ptr)::Ptr{Float64} │ %26 = Base.bitcast(Ptr{Nothing}, %25)::Ptr{Nothing} │ $(Expr(:foreigncall, :(:free), Nothing, svec(Ptr{Nothing}), 0, :(:ccall), :(%26), :(%25)))::Nothing └─── return nothing ) => Nothing ``` However, this is still a WIP. Before merging, I want to improve EA's precision a bit and at least fix the test case that is currently marked as `broken`. I also need to check its impact on compiler performance. Additionally, I believe this feature is not yet practical. In particular, there is still significant room for improvement in the following areas: - EA's interprocedural capabilities: currently EA is performed ad-hoc for limited frames because of latency reasons, which significantly reduces its precision in the presence of interprocedural calls. - Relaxing the `:nothrow` check for finalizer inlining: the current algorithm requires `:nothrow`-ness on all paths from the allocation of the mutable struct to its last use, which is not practical for real-world cases. Even when `:nothrow` cannot be guaranteed, auxiliary optimizations such as inserting a `finalize` call after the last use might still be possible (JuliaLang#55990).
1 parent f5937b4 commit b19a7c1

File tree

7 files changed

+225
-101
lines changed

7 files changed

+225
-101
lines changed

base/compiler/optimize.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ function refine_effects!(interp::AbstractInterpreter, opt::OptimizationState, sv
665665
if !is_effect_free(sv.result.ipo_effects) && sv.all_effect_free && !isempty(sv.ea_analysis_pending)
666666
ir = sv.ir
667667
nargs = Int(opt.src.nargs)
668-
estate = EscapeAnalysis.analyze_escapes(ir, nargs, optimizer_lattice(interp), GetNativeEscapeCache(interp))
668+
estate = EscapeAnalysis.analyze_escapes(ir, nargs, optimizer_lattice(interp), get_escape_cache(interp))
669669
argescapes = EscapeAnalysis.ArgEscapeCache(estate)
670670
stack_analysis_result!(sv.result, argescapes)
671671
validate_mutable_arg_escapes!(estate, sv)

base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl

+41-18
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import ._TOP_MOD: ==, getindex, setindex!
1818
using Core: MethodMatch, SimpleVector, ifelse, sizeof
1919
using Core.IR
2020
using ._TOP_MOD: # Base definitions
21-
@__MODULE__, @assert, @eval, @goto, @inbounds, @inline, @label, @noinline,
21+
@__MODULE__, @assert, @eval, @goto, @inbounds, @inline, @label, @noinline, @show,
2222
@nospecialize, @specialize, BitSet, Callable, Csize_t, IdDict, IdSet, UnitRange, Vector,
2323
copy, delete!, empty!, enumerate, error, first, get, get!, haskey, in, isassigned,
2424
isempty, ismutabletype, keys, last, length, max, min, missing, pop!, push!, pushfirst!,
@@ -657,11 +657,13 @@ function analyze_escapes(ir::IRCode, nargs::Int, 𝕃ₒ::AbstractLattice, get_e
657657
# `escape_exception!` conservatively propagates `AllEscape` anyway,
658658
# and so escape information imposed on `:the_exception` isn't computed
659659
continue
660+
elseif head === :gc_preserve_begin
661+
# GC preserve is handled by `escape_gc_preserve!`
662+
elseif head === :gc_preserve_end
663+
escape_gc_preserve!(astate, pc, stmt.args)
660664
elseif head === :static_parameter || # this exists statically, not interested in its escape
661-
head === :copyast || # XXX can this account for some escapes?
662-
head === :isdefined || # just returns `Bool`, nothing accounts for any escapes
663-
head === :gc_preserve_begin || # `GC.@preserve` expressions themselves won't be used anywhere
664-
head === :gc_preserve_end # `GC.@preserve` expressions themselves won't be used anywhere
665+
head === :copyast || # XXX escape something?
666+
head === :isdefined # just returns `Bool`, nothing accounts for any escapes
665667
continue
666668
else
667669
add_conservative_changes!(astate, pc, stmt.args)
@@ -1064,17 +1066,27 @@ end
10641066
function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any})
10651067
mi = first(args)::MethodInstance
10661068
first_idx, last_idx = 2, length(args)
1069+
add_liveness_changes!(astate, pc, args, first_idx, last_idx)
10671070
# TODO inspect `astate.ir.stmts[pc][:info]` and use const-prop'ed `InferenceResult` if available
10681071
cache = astate.get_escape_cache(mi)
1072+
ret = SSAValue(pc)
10691073
if cache isa Bool
10701074
if cache
1071-
return nothing # guaranteed to have no escape
1075+
# This method call is very simple and has good effects, so there's no need to
1076+
# escape its arguments. However, since the arguments might be returned, we need
1077+
# to consider the possibility of aliasing between them and the return value.
1078+
for argidx = first_idx:last_idx
1079+
arg = args[argidx]
1080+
if !is_mutation_free_argtype(argextype(arg, astate.ir))
1081+
add_alias_change!(astate, ret, arg)
1082+
end
1083+
end
1084+
return nothing
10721085
else
10731086
return add_conservative_changes!(astate, pc, args, 2)
10741087
end
10751088
end
10761089
cache = cache::ArgEscapeCache
1077-
ret = SSAValue(pc)
10781090
retinfo = astate.estate[ret] # escape information imposed on the call statement
10791091
method = mi.def::Method
10801092
nargs = Int(method.nargs)
@@ -1162,6 +1174,17 @@ function escape_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any})
11621174
end
11631175
end
11641176

1177+
function escape_gc_preserve!(astate::AnalysisState, pc::Int, args::Vector{Any})
1178+
@assert length(args) == 1 "invalid :gc_preserve_end"
1179+
val = args[1]
1180+
@assert val isa SSAValue "invalid :gc_preserve_end"
1181+
beginstmt = astate.ir[val][:stmt]
1182+
@assert isexpr(beginstmt, :gc_preserve_begin) "invalid :gc_preserve_end"
1183+
beginargs = beginstmt.args
1184+
# COMBAK we might need to add liveness for all statements from `:gc_preserve_begin` to `:gc_preserve_end`
1185+
add_liveness_changes!(astate, pc, beginargs)
1186+
end
1187+
11651188
normalize(@nospecialize x) = isa(x, QuoteNode) ? x.value : x
11661189

11671190
function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any})
@@ -1187,20 +1210,12 @@ function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any})
11871210
if result === missing
11881211
# if this call hasn't been handled by any of pre-defined handlers, escape it conservatively
11891212
add_conservative_changes!(astate, pc, args)
1190-
return
11911213
elseif result === true
11921214
add_liveness_changes!(astate, pc, args, 2)
1193-
return # ThrownEscape is already checked
1215+
elseif is_nothrow(astate.ir, pc)
1216+
add_liveness_changes!(astate, pc, args, 2)
11941217
else
1195-
# we escape statements with the `ThrownEscape` property using the effect-freeness
1196-
# computed by `stmt_effect_flags` invoked within inlining
1197-
# TODO throwness ≠ "effect-free-ness"
1198-
if is_nothrow(astate.ir, pc)
1199-
add_liveness_changes!(astate, pc, args, 2)
1200-
else
1201-
add_fallback_changes!(astate, pc, args, 2)
1202-
end
1203-
return
1218+
add_fallback_changes!(astate, pc, args, 2)
12041219
end
12051220
end
12061221

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

1546+
function escape_builtin!(::typeof(Core.finalizer), astate::AnalysisState, pc::Int, args::Vector{Any})
1547+
if length(args) 3
1548+
obj = args[3]
1549+
add_liveness_change!(astate, obj, pc) # TODO setup a proper FinalizerEscape?
1550+
end
1551+
return false
1552+
end
1553+
15311554
end # baremodule EscapeAnalysis

base/compiler/ssair/passes.jl

+60-26
Original file line numberDiff line numberDiff line change
@@ -1300,7 +1300,13 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing)
13001300
# Inlining performs legality checks on the finalizer to determine
13011301
# whether or not we may inline it. If so, it appends extra arguments
13021302
# at the end of the intrinsic. Detect that here.
1303-
length(stmt.args) == 5 || continue
1303+
if length(stmt.args) == 4 && stmt.args[4] === nothing
1304+
# constant case
1305+
elseif length(stmt.args) == 5 && stmt.args[4] isa Bool && stmt.args[5] isa MethodInstance
1306+
# inlining case
1307+
else
1308+
continue
1309+
end
13041310
end
13051311
is_finalizer = true
13061312
elseif isexpr(stmt, :foreigncall)
@@ -1685,18 +1691,21 @@ end
16851691
function sroa_mutables!(ir::IRCode, defuses::IdDict{Int,Tuple{SPCSet,SSADefUse}}, used_ssas::Vector{Int}, lazydomtree::LazyDomtree, inlining::Union{Nothing,InliningState})
16861692
𝕃ₒ = inlining === nothing ? SimpleInferenceLattice.instance : optimizer_lattice(inlining.interp)
16871693
lazypostdomtree = LazyPostDomtree(ir)
1688-
for (defidx, (intermediaries, defuse)) in defuses
1689-
# Check if there are any uses we did not account for. If so, the variable
1690-
# escapes and we cannot eliminate the allocation. This works, because we're guaranteed
1691-
# not to include any intermediaries that have dead uses. As a result, missing uses will only ever
1692-
# show up in the nuses_total count.
1693-
nleaves = length(defuse.uses) + length(defuse.defs)
1694-
nuses = 0
1695-
for iidx in intermediaries
1696-
nuses += used_ssas[iidx]
1694+
function find_finalizer_useidx(defuse::SSADefUse)
1695+
finalizer_useidx = nothing
1696+
for (useidx, use) in enumerate(defuse.uses)
1697+
if use.kind === :finalizer
1698+
# For now: Only allow one finalizer per allocation
1699+
finalizer_useidx !== nothing && return false
1700+
finalizer_useidx = useidx
1701+
end
16971702
end
1698-
nuses_total = used_ssas[defidx] + nuses - length(intermediaries)
1699-
nleaves == nuses_total || continue
1703+
if finalizer_useidx === nothing || inlining === nothing
1704+
return true
1705+
end
1706+
return finalizer_useidx
1707+
end
1708+
for (defidx, (intermediaries, defuse)) in defuses
17001709
# Find the type for this allocation
17011710
defexpr = ir[SSAValue(defidx)][:stmt]
17021711
isexpr(defexpr, :new) || continue
@@ -1706,22 +1715,47 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int,Tuple{SPCSet,SSADefUse}}
17061715
typ = widenconst(typ)
17071716
ismutabletype(typ) || continue
17081717
typ = typ::DataType
1709-
# First check for any finalizer calls
1710-
finalizer_useidx = nothing
1711-
for (useidx, use) in enumerate(defuse.uses)
1712-
if use.kind === :finalizer
1713-
# For now: Only allow one finalizer per allocation
1714-
finalizer_useidx !== nothing && @goto skip
1715-
finalizer_useidx = useidx
1716-
end
1718+
# Check if there are any uses we did not account for. If so, the variable
1719+
# escapes and we cannot eliminate the allocation. This works, because we're guaranteed
1720+
# not to include any intermediaries that have dead uses. As a result, missing uses will only ever
1721+
# show up in the nuses_total count.
1722+
nleaves = length(defuse.uses) + length(defuse.defs)
1723+
nuses = 0
1724+
for iidx in intermediaries
1725+
nuses += used_ssas[iidx]
17171726
end
1727+
nuses_total = used_ssas[defidx] + nuses - length(intermediaries)
17181728
all_eliminated = all_forwarded = true
1719-
if finalizer_useidx !== nothing && inlining !== nothing
1720-
finalizer_idx = defuse.uses[finalizer_useidx].idx
1721-
try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining,
1722-
lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info])
1723-
deleteat!(defuse.uses, finalizer_useidx)
1724-
all_eliminated = all_forwarded = false # can't eliminate `setfield!` calls safely
1729+
if nleaves nuses_total
1730+
finalizer_useidx = find_finalizer_useidx(defuse)
1731+
if finalizer_useidx isa Int
1732+
nargs = length(ir.argtypes) # COMBAK this might need to be `Int(opt.src.nargs)`
1733+
estate = EscapeAnalysis.analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache(inlining.interp))
1734+
einfo = estate[SSAValue(defidx)]
1735+
if EscapeAnalysis.has_no_escape(einfo)
1736+
already = BitSet(use.idx for use in defuse.uses)
1737+
for idx = einfo.Liveness
1738+
if idx already
1739+
push!(defuse.uses, SSAUse(:EALiveness, idx))
1740+
end
1741+
end
1742+
finalizer_idx = defuse.uses[finalizer_useidx].idx
1743+
try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining::InliningState,
1744+
lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info])
1745+
end
1746+
end
1747+
continue
1748+
else
1749+
finalizer_useidx = find_finalizer_useidx(defuse)
1750+
if finalizer_useidx isa Int
1751+
finalizer_idx = defuse.uses[finalizer_useidx].idx
1752+
try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining::InliningState,
1753+
lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info])
1754+
deleteat!(defuse.uses, finalizer_useidx)
1755+
all_eliminated = all_forwarded = false # can't eliminate `setfield!` calls safely
1756+
elseif !finalizer_useidx
1757+
continue
1758+
end
17251759
end
17261760
# Partition defuses by field
17271761
fielddefuse = SSADefUse[SSADefUse() for _ = 1:fieldcount(typ)]

base/compiler/types.jl

+2
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,8 @@ typeinf_lattice(::AbstractInterpreter) = InferenceLattice(BaseInferenceLattice.i
457457
ipo_lattice(::AbstractInterpreter) = InferenceLattice(IPOResultLattice.instance)
458458
optimizer_lattice(::AbstractInterpreter) = SimpleInferenceLattice.instance
459459

460+
get_escape_cache(interp::AbstractInterpreter) = GetNativeEscapeCache(interp)
461+
460462
abstract type CallInfo end
461463

462464
@nospecialize

0 commit comments

Comments
 (0)