From 00672b388c3a393fe0d39efe5d330fd791d81909 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Sat, 16 Sep 2023 17:29:07 +0900 Subject: [PATCH] EA: perform analysis once for post-optimization IR, and remove `IPO EA` (#51318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following the discussions and changes in #50805, we now consider post-inlining IR as IPO-valid. Revisiting EA, I've realized that running EA twice—once for computing IPO-valid escape cache and once for local optimization analysis—is redundant. This commit streamlines the EA process to perform the analysis just once on post-optimization IR, and caches that result. This change also removes all interprocedural EA code, which had significant overlap with inlining code. --------- Co-authored-by: Julian Samaroo --- base/compiler/bootstrap.jl | 2 +- base/compiler/optimize.jl | 26 -- .../ssair/EscapeAnalysis/EscapeAnalysis.jl | 176 ++++------- .../ssair/EscapeAnalysis/interprocedural.jl | 159 ---------- test/choosetests.jl | 4 +- test/compiler/EscapeAnalysis/EAUtils.jl | 212 +++++++------ .../{local.jl => EscapeAnalysis.jl} | 291 ++++++++++++++---- .../EscapeAnalysis/interprocedural.jl | 275 ----------------- test/compiler/EscapeAnalysis/setup.jl | 38 --- 9 files changed, 395 insertions(+), 788 deletions(-) delete mode 100644 base/compiler/ssair/EscapeAnalysis/interprocedural.jl rename test/compiler/EscapeAnalysis/{local.jl => EscapeAnalysis.jl} (90%) delete mode 100644 test/compiler/EscapeAnalysis/interprocedural.jl delete mode 100644 test/compiler/EscapeAnalysis/setup.jl diff --git a/base/compiler/bootstrap.jl b/base/compiler/bootstrap.jl index a3bcab0adecff..4b5887a82d046 100644 --- a/base/compiler/bootstrap.jl +++ b/base/compiler/bootstrap.jl @@ -9,7 +9,7 @@ time() = ccall(:jl_clock_now, Float64, ()) let interp = NativeInterpreter() - # analyze_escapes_tt = Tuple{typeof(analyze_escapes), IRCode, Int, Bool, typeof(null_escape_cache)} + # analyze_escapes_tt = Tuple{typeof(analyze_escapes), IRCode, Int, Bool, TODO} fs = Any[ # we first create caches for the optimizer, because they contain many loop constructions # and they're better to not run in interpreter even during bootstrapping diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index e85ce8cd44d28..36ef4561fc339 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -788,32 +788,6 @@ function optimize(interp::AbstractInterpreter, opt::OptimizationState, caller::I return finish(interp, opt, ir, caller) end -using .EscapeAnalysis -import .EscapeAnalysis: EscapeState, ArgEscapeCache, is_ipo_profitable - -""" - cache_escapes!(caller::InferenceResult, estate::EscapeState) - -Transforms escape information of call arguments of `caller`, -and then caches it into a global cache for later interprocedural propagation. -""" -cache_escapes!(caller::InferenceResult, estate::EscapeState) = - caller.argescapes = ArgEscapeCache(estate) - -function ipo_escape_cache(mi_cache::MICache) where MICache - return function (linfo::Union{InferenceResult,MethodInstance}) - if isa(linfo, InferenceResult) - argescapes = linfo.argescapes - else - codeinst = get(mi_cache, linfo, nothing) - isa(codeinst, CodeInstance) || return nothing - argescapes = codeinst.argescapes - end - return argescapes !== nothing ? argescapes::ArgEscapeCache : nothing - end -end -null_escape_cache(linfo::Union{InferenceResult,MethodInstance}) = nothing - macro pass(name, expr) optimize_until = esc(:optimize_until) stage = esc(:__stage__) diff --git a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl index ba4ce1212b965..4c533367017ff 100644 --- a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl +++ b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl @@ -40,7 +40,6 @@ else end const AInfo = IdSet{Any} -const LivenessSet = BitSet const 𝕃ₒ = SimpleInferenceLattice.instance """ @@ -87,16 +86,16 @@ An abstract state will be initialized with the bottom(-like) elements: struct EscapeInfo Analyzed::Bool ReturnEscape::Bool - ThrownEscape::LivenessSet + ThrownEscape::BitSet AliasInfo #::Union{IndexableFields,IndexableElements,Unindexable,Bool} - Liveness::LivenessSet + Liveness::BitSet function EscapeInfo( Analyzed::Bool, ReturnEscape::Bool, - ThrownEscape::LivenessSet, + ThrownEscape::BitSet, AliasInfo#=::Union{IndexableFields,IndexableElements,Unindexable,Bool}=#, - Liveness::LivenessSet) + Liveness::BitSet) @nospecialize AliasInfo return new( Analyzed, @@ -112,8 +111,8 @@ struct EscapeInfo AliasInfo#=::Union{IndexableFields,IndexableElements,Unindexable,Bool}=# = x.AliasInfo; Analyzed::Bool = x.Analyzed, ReturnEscape::Bool = x.ReturnEscape, - ThrownEscape::LivenessSet = x.ThrownEscape, - Liveness::LivenessSet = x.Liveness) + ThrownEscape::BitSet = x.ThrownEscape, + Liveness::BitSet = x.Liveness) @nospecialize AliasInfo return new( Analyzed, @@ -126,24 +125,24 @@ end # precomputed default values in order to eliminate computations at each callsite -const BOT_THROWN_ESCAPE = LivenessSet() +const BOT_THROWN_ESCAPE = BitSet() # NOTE the lattice operations should try to avoid actual set computations on this top value, -# and e.g. LivenessSet(0:1000000) should also work without incurring excessive computations -const TOP_THROWN_ESCAPE = LivenessSet(-1) +# and e.g. BitSet(0:1000000) should also work without incurring excessive computations +const TOP_THROWN_ESCAPE = BitSet(-1) -const BOT_LIVENESS = LivenessSet() +const BOT_LIVENESS = BitSet() # NOTE the lattice operations should try to avoid actual set computations on this top value, -# and e.g. LivenessSet(0:1000000) should also work without incurring excessive computations -const TOP_LIVENESS = LivenessSet(-1:0) -const ARG_LIVENESS = LivenessSet(0) +# and e.g. BitSet(0:1000000) should also work without incurring excessive computations +const TOP_LIVENESS = BitSet(-1:0) +const ARG_LIVENESS = BitSet(0) # the constructors NotAnalyzed() = EscapeInfo(false, false, BOT_THROWN_ESCAPE, false, BOT_LIVENESS) # not formally part of the lattice NoEscape() = EscapeInfo(true, false, BOT_THROWN_ESCAPE, false, BOT_LIVENESS) ArgEscape() = EscapeInfo(true, false, BOT_THROWN_ESCAPE, true, ARG_LIVENESS) -ReturnEscape(pc::Int) = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, LivenessSet(pc)) +ReturnEscape(pc::Int) = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, BitSet(pc)) AllReturnEscape() = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, TOP_LIVENESS) -ThrownEscape(pc::Int) = EscapeInfo(true, false, LivenessSet(pc), false, BOT_LIVENESS) +ThrownEscape(pc::Int) = EscapeInfo(true, false, BitSet(pc), false, BOT_LIVENESS) AllEscape() = EscapeInfo(true, true, TOP_THROWN_ESCAPE, true, TOP_LIVENESS) const ⊥, ⊤ = NotAnalyzed(), AllEscape() @@ -626,7 +625,7 @@ struct LivenessChange <: Change end const Changes = Vector{Change} -struct AnalysisState{T<:Callable} +struct AnalysisState{T} ir::IRCode estate::EscapeState changes::Changes @@ -634,20 +633,18 @@ struct AnalysisState{T<:Callable} end """ - analyze_escapes(ir::IRCode, nargs::Int, call_resolved::Bool, get_escape_cache::Callable) - -> estate::EscapeState + analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> estate::EscapeState Analyzes escape information in `ir`: - `nargs`: the number of actual arguments of the analyzed call -- `call_resolved`: if interprocedural calls are already resolved by `ssa_inlining_pass!` -- `get_escape_cache(::Union{InferenceResult,MethodInstance}) -> Union{Nothing,ArgEscapeCache}`: +- `get_escape_cache(::MethodInstance) -> Union{Nothing,ArgEscapeCache}`: retrieves cached argument escape information """ -function analyze_escapes(ir::IRCode, nargs::Int, call_resolved::Bool, get_escape_cache::T) where T<:Callable +function analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) stmts = ir.stmts nstmts = length(stmts) + length(ir.new_nodes.stmts) - tryregions, arrayinfo, callinfo = compute_frameinfo(ir, call_resolved) + tryregions, arrayinfo = compute_frameinfo(ir) estate = EscapeState(nargs, nstmts, arrayinfo) changes = Changes() # keeps changes that happen at current statement astate = AnalysisState(ir, estate, changes, get_escape_cache) @@ -663,11 +660,7 @@ function analyze_escapes(ir::IRCode, nargs::Int, call_resolved::Bool, get_escape if isa(stmt, Expr) head = stmt.head if head === :call - if callinfo !== nothing - escape_call!(astate, pc, stmt.args, callinfo) - else - escape_call!(astate, pc, stmt.args) - end + escape_call!(astate, pc, stmt.args) elseif head === :invoke escape_invoke!(astate, pc, stmt.args) elseif head === :new || head === :splatnew @@ -744,15 +737,11 @@ function analyze_escapes(ir::IRCode, nargs::Int, call_resolved::Bool, get_escape end """ - compute_frameinfo(ir::IRCode, call_resolved::Bool) -> (tryregions, arrayinfo, callinfo) + compute_frameinfo(ir::IRCode) -> (tryregions, arrayinfo) A preparatory linear scan before the escape analysis on `ir` to find: - `tryregions::Union{Nothing,Vector{UnitRange{Int}}}`: regions in which potential `throw`s can be caught (used by `escape_exception!`) - `arrayinfo::Union{Nothing,IdDict{Int,Vector{Int}}}`: array allocations whose dimensions are known precisely (with some very simple local analysis) -- `callinfo::`: when `!call_resolved`, `compute_frameinfo` additionally returns `callinfo::Vector{Union{MethodInstance,InferenceResult}}`, - which contains information about statically resolved callsites. - The inliner will use essentially equivalent interprocedural information to inline callees as well as resolve static callsites, - this additional information won't be required when analyzing post-inlining IR. !!! note This array dimension analysis to compute `arrayinfo` is very local and doesn't account @@ -760,25 +749,13 @@ A preparatory linear scan before the escape analysis on `ir` to find: Ideally this dimension analysis should be done as a part of type inference that propagates array dimensions in a flow sensitive way. """ -function compute_frameinfo(ir::IRCode, call_resolved::Bool) +function compute_frameinfo(ir::IRCode) nstmts, nnewnodes = length(ir.stmts), length(ir.new_nodes.stmts) tryregions, arrayinfo = nothing, nothing - if !call_resolved - callinfo = Vector{Any}(undef, nstmts+nnewnodes) - else - callinfo = nothing - end for idx in 1:nstmts+nnewnodes inst = ir[SSAValue(idx)] stmt = inst[:stmt] - if !call_resolved - # TODO don't call `check_effect_free!` in the inlinear - check_effect_free!(ir, idx, stmt, inst[:type], 𝕃ₒ) - end - if callinfo !== nothing && isexpr(stmt, :call) - # TODO: pass effects here - callinfo[idx] = resolve_call(ir, stmt, inst[:info]) - elseif isexpr(stmt, :enter) + if isexpr(stmt, :enter) @assert idx ≤ nstmts "try/catch inside new_nodes unsupported" tryregions === nothing && (tryregions = UnitRange{Int}[]) leave_block = stmt.args[1]::Int @@ -851,14 +828,7 @@ function compute_frameinfo(ir::IRCode, call_resolved::Bool) end @label next_stmt end - return tryregions, arrayinfo, callinfo -end - -# define resolve_call -if _TOP_MOD === Core.Compiler - include("compiler/ssair/EscapeAnalysis/interprocedural.jl") -else - include("interprocedural.jl") + return tryregions, arrayinfo end # propagate changes, and check convergence @@ -906,7 +876,7 @@ end return false end -# propagate Liveness changes separately in order to avoid constructing too many LivenessSet +# propagate Liveness changes separately in order to avoid constructing too many BitSet @inline function propagate_liveness_change!(estate::EscapeState, change::LivenessChange) (; xidx, livepc) = change info = estate.escapes[xidx] @@ -1149,13 +1119,9 @@ escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) = escape_invoke!(astate, pc, args, first(args)::MethodInstance, 2) function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}, - linfo::Linfo, first_idx::Int, last_idx::Int = length(args)) - if isa(linfo, InferenceResult) - cache = astate.get_escape_cache(linfo) - linfo = linfo.linfo - else - cache = astate.get_escape_cache(linfo) - end + mi::MethodInstance, first_idx::Int, last_idx::Int = length(args)) + # TODO inspect `astate.ir.stmts[pc][:info]` and use const-prop'ed `InferenceResult` if available + cache = astate.get_escape_cache(mi) if cache === nothing return add_conservative_changes!(astate, pc, args, 2) else @@ -1163,7 +1129,7 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}, end ret = SSAValue(pc) retinfo = astate.estate[ret] # escape information imposed on the call statement - method = linfo.def::Method + method = mi.def::Method nargs = Int(method.nargs) for (i, argidx) in enumerate(first_idx:last_idx) arg = args[argidx] @@ -1172,18 +1138,14 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}, # COMBAK will this be invalid once we take alias information into account? i = nargs end - arginfo = cache.argescapes[i] - info = from_interprocedural(arginfo, pc) - if has_return_escape(arginfo) - # if this argument can be "returned", in addition to propagating - # the escape information imposed on this call argument within the callee, - # we should also account for possible aliasing of this argument and the returned value - add_escape_change!(astate, arg, info) + argescape = cache.argescapes[i] + info = from_interprocedural(argescape, pc) + # propagate the escape information imposed on this call argument by the callee + add_escape_change!(astate, arg, info) + if has_return_escape(argescape) + # if this argument can be "returned", we should also account for possible + # aliasing between this argument and the returned value add_alias_change!(astate, ret, arg) - else - # if this is simply passed as the call argument, we can just propagate - # the escape information imposed on this call argument within the callee - add_escape_change!(astate, arg, info) end end for (; aidx, bidx) in cache.argaliases @@ -1194,24 +1156,22 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}, end """ - from_interprocedural(arginfo::ArgEscapeInfo, pc::Int) -> x::EscapeInfo + from_interprocedural(argescape::ArgEscapeInfo, pc::Int) -> x::EscapeInfo -Reinterprets the escape information imposed on the call argument which is cached as `arginfo` +Reinterprets the escape information imposed on the call argument which is cached as `argescape` in the context of the caller frame, where `pc` is the SSA statement number of the return value. """ -function from_interprocedural(arginfo::ArgEscapeInfo, pc::Int) - has_all_escape(arginfo) && return ⊤ - - ThrownEscape = has_thrown_escape(arginfo) ? LivenessSet(pc) : BOT_THROWN_ESCAPE - - return EscapeInfo( - #=Analyzed=#true, #=ReturnEscape=#false, ThrownEscape, - # FIXME implement interprocedural memory effect-analysis - # currently, this essentially disables the entire field analysis - # it might be okay from the SROA point of view, since we can't remove the allocation - # as far as it's passed to a callee anyway, but still we may want some field analysis - # for e.g. stack allocation or some other IPO optimizations - #=AliasInfo=#true, #=Liveness=#LivenessSet(pc)) +function from_interprocedural(argescape::ArgEscapeInfo, pc::Int) + has_all_escape(argescape) && return ⊤ + ThrownEscape = has_thrown_escape(argescape) ? BitSet(pc) : BOT_THROWN_ESCAPE + # TODO implement interprocedural memory effect-analysis: + # currently, this essentially disables the entire field analysis–it might be okay from + # the SROA point of view, since we can't remove the allocation as far as it's passed to + # a callee anyway, but still we may want some field analysis for e.g. stack allocation + # or some other IPO optimizations + AliasInfo = true + Liveness = BitSet(pc) + return EscapeInfo(#=Analyzed=#true, #=ReturnEscape=#false, ThrownEscape, AliasInfo, Liveness) end # escape every argument `(args[6:length(args[3])])` and the name `args[1]` @@ -1270,27 +1230,6 @@ end normalize(@nospecialize x) = isa(x, QuoteNode) ? x.value : x -function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}, callinfo::Vector{Any}) - info = callinfo[pc] - if isa(info, Bool) - info && return # known to be no escape - # now cascade to the builtin handling - escape_call!(astate, pc, args) - return - elseif isa(info, EACallInfo) - for linfo in info.linfos - escape_invoke!(astate, pc, args, linfo, 1) - end - # accounts for a potential escape via MethodError - info.nothrow || add_thrown_escapes!(astate, pc, args) - return - else - @assert info === missing - # if this call couldn't be analyzed, escape it conservatively - add_conservative_changes!(astate, pc, args) - end -end - function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) ir = astate.ir ft = argextype(first(args), ir, ir.sptypes, ir.argtypes) @@ -1331,16 +1270,17 @@ function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) end end -escape_builtin!(@nospecialize(f), _...) = return missing +escape_builtin!(@nospecialize(f), _...) = missing # safe builtins -escape_builtin!(::typeof(isa), _...) = return false -escape_builtin!(::typeof(typeof), _...) = return false -escape_builtin!(::typeof(sizeof), _...) = return false -escape_builtin!(::typeof(===), _...) = return false +escape_builtin!(::typeof(isa), _...) = false +escape_builtin!(::typeof(typeof), _...) = false +escape_builtin!(::typeof(sizeof), _...) = false +escape_builtin!(::typeof(===), _...) = false +escape_builtin!(::typeof(Core.donotdelete), _...) = false # not really safe, but `ThrownEscape` will be imposed later -escape_builtin!(::typeof(isdefined), _...) = return false -escape_builtin!(::typeof(throw), _...) = return false +escape_builtin!(::typeof(isdefined), _...) = false +escape_builtin!(::typeof(throw), _...) = false function escape_builtin!(::typeof(ifelse), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) == 4 || return false diff --git a/base/compiler/ssair/EscapeAnalysis/interprocedural.jl b/base/compiler/ssair/EscapeAnalysis/interprocedural.jl deleted file mode 100644 index 26b0e5b404641..0000000000000 --- a/base/compiler/ssair/EscapeAnalysis/interprocedural.jl +++ /dev/null @@ -1,159 +0,0 @@ -# TODO this file contains many duplications with the inlining analysis code, factor them out - -import Core.Compiler: - MethodInstance, InferenceResult, Signature, ConstPropResult, ConcreteResult, - SemiConcreteResult, CallInfo, NoCallInfo, MethodResultPure, MethodMatchInfo, - UnionSplitInfo, ConstCallInfo, InvokeCallInfo, - call_sig, argtypes_to_type, is_builtin, is_return_type, istopfunction, - validate_sparams, specialize_method, invoke_rewrite - -const Linfo = Union{MethodInstance,InferenceResult} -struct EACallInfo - linfos::Vector{Linfo} - nothrow::Bool -end - -function resolve_call(ir::IRCode, stmt::Expr, @nospecialize(info::CallInfo)) - # TODO: if effect free, return true - sig = call_sig(ir, stmt) - if sig === nothing - return missing - end - # TODO handle _apply_iterate - if is_builtin(𝕃ₒ, sig) && sig.f !== invoke - return false - end - # handling corresponding to late_inline_special_case! - (; f, argtypes) = sig - if length(argtypes) == 3 && istopfunction(f, :!==) - return true - elseif length(argtypes) == 3 && istopfunction(f, :(>:)) - return true - elseif f === TypeVar && 2 ≤ length(argtypes) ≤ 4 && (argtypes[2] ⊑ Symbol) - return true - elseif f === UnionAll && length(argtypes) == 3 && (argtypes[2] ⊑ TypeVar) - return true - elseif is_return_type(f) - return true - end - if info isa MethodResultPure - return true - elseif info === NoCallInfo - return missing - end - # TODO handle OpaqueClosureCallInfo - if sig.f === invoke - isa(info, InvokeCallInfo) || return missing - return analyze_invoke_call(sig, info) - elseif isa(info, ConstCallInfo) - return analyze_const_call(sig, info) - elseif isa(info, MethodMatchInfo) - infos = MethodMatchInfo[info] - elseif isa(info, UnionSplitInfo) - infos = info.matches - else # isa(info, ReturnTypeCallInfo), etc. - return missing - end - return analyze_call(sig, infos) -end - -function analyze_invoke_call(sig::Signature, info::InvokeCallInfo) - match = info.match - if !match.fully_covers - # TODO: We could union split out the signature check and continue on - return missing - end - result = info.result - if isa(result, ConstPropResult) - return EACallInfo(Linfo[result.result], true) - elseif isa(result, ConcreteResult) - return EACallInfo(Linfo[result.mi], true) - elseif isa(result, SemiConcreteResult) - return EACallInfo(Linfo[result.mi], true) - else - argtypes = invoke_rewrite(sig.argtypes) - mi = analyze_match(match, length(argtypes)) - mi === nothing && return missing - return EACallInfo(Linfo[mi], true) - end -end - -function analyze_const_call(sig::Signature, cinfo::ConstCallInfo) - linfos = Linfo[] - (; call, results) = cinfo - infos = isa(call, MethodMatchInfo) ? MethodMatchInfo[call] : call.matches - local nothrow = true # required to account for potential escape via MethodError - local j = 0 - for i in 1:length(infos) - meth = infos[i].results - nothrow &= !meth.ambig - nmatch = Core.Compiler.length(meth) - if nmatch == 0 # No applicable methods - # mark this call may potentially throw, and the try next union split - nothrow = false - continue - end - for i = 1:nmatch - j += 1 - result = results[j] - match = Core.Compiler.getindex(meth, i) - if result === nothing - mi = analyze_match(match, length(sig.argtypes)) - mi === nothing && return missing - push!(linfos, mi) - elseif isa(result, ConcreteResult) - # TODO we may want to feedback information that this call always throws if !isdefined(result, :result) - push!(linfos, result.mi) - elseif isa(result, SemiConcreteResult) - push!(linfos, result.mi) - elseif isa(result, ConstPropResult) - push!(linfos, result.result) - end - nothrow &= match.fully_covers - end - end - return EACallInfo(linfos, nothrow) -end - -function analyze_call(sig::Signature, infos::Vector{MethodMatchInfo}) - linfos = Linfo[] - local nothrow = true # required to account for potential escape via MethodError - for i in 1:length(infos) - meth = infos[i].results - nothrow &= !meth.ambig - nmatch = Core.Compiler.length(meth) - if nmatch == 0 # No applicable methods - # mark this call may potentially throw, and the try next union split - nothrow = false - continue - end - for i = 1:nmatch - match = Core.Compiler.getindex(meth, i) - mi = analyze_match(match, length(sig.argtypes)) - mi === nothing && return missing - push!(linfos, mi) - nothrow &= match.fully_covers - end - end - return EACallInfo(linfos, nothrow) -end - -function analyze_match(match::MethodMatch, npassedargs::Int) - method = match.method - na = Int(method.nargs) - if na != npassedargs && !(na > 0 && method.isva) - # we have a method match only because an earlier - # inference step shortened our call args list, even - # though we have too many arguments to actually - # call this function - return nothing - end - - # Bail out if any static parameters are left as TypeVar - # COMBAK is this needed for escape analysis? - validate_sparams(match.sparams) || return nothing - - # See if there exists a specialization for this method signature - mi = specialize_method(match; preexisting=true) # Union{Nothing, MethodInstance} - return mi -end diff --git a/test/choosetests.jl b/test/choosetests.jl index c38817bb4eeb9..3139ea60270e3 100644 --- a/test/choosetests.jl +++ b/test/choosetests.jl @@ -158,9 +158,9 @@ function choosetests(choices = []) "compiler/validation", "compiler/ssair", "compiler/irpasses", "compiler/codegen", "compiler/inline", "compiler/contextual", "compiler/invalidation", "compiler/AbstractInterpreter", - "compiler/EscapeAnalysis/local", "compiler/EscapeAnalysis/interprocedural"]) + "compiler/EscapeAnalysis/EscapeAnalysis"]) filtertests!(tests, "compiler/EscapeAnalysis", [ - "compiler/EscapeAnalysis/local", "compiler/EscapeAnalysis/interprocedural"]) + "compiler/EscapeAnalysis/EscapeAnalysis"]) filtertests!(tests, "stdlib", STDLIBS) filtertests!(tests, "internet_required", INTERNET_REQUIRED_LIST) # do ambiguous first to avoid failing if ambiguities are introduced by other tests diff --git a/test/compiler/EscapeAnalysis/EAUtils.jl b/test/compiler/EscapeAnalysis/EAUtils.jl index 0848b434171f5..a8a3fec68b34f 100644 --- a/test/compiler/EscapeAnalysis/EAUtils.jl +++ b/test/compiler/EscapeAnalysis/EAUtils.jl @@ -9,7 +9,7 @@ const EA = EscapeAnalysis # entries # ------- -using Base: unwrap_unionall, rewrap_unionall +using Base: IdSet, unwrap_unionall, rewrap_unionall using InteractiveUtils: gen_call_with_extracted_types_and_kwargs """ @@ -39,13 +39,12 @@ Runs the escape analysis on optimized IR of a generic function call with the giv """ function code_escapes(@nospecialize(f), @nospecialize(types=Base.default_tt(f)); world::UInt = get_world_counter(), - debuginfo::Symbol = :none, - optimize::Bool = true) + debuginfo::Symbol = :none) tt = Base.signature_type(f, types) - interp = EscapeAnalyzer(world, tt, optimize) + interp = EscapeAnalyzer(world, tt) results = Base.code_typed_by_type(tt; optimize=true, world, interp) isone(length(results)) || throw(ArgumentError("`code_escapes` only supports single analysis result")) - return EscapeResult(interp.ir, interp.state, interp.mi, debuginfo === :source) + return EscapeResult(interp.ir, interp.estate, interp.mi, debuginfo === :source, interp) end # in order to run a whole analysis from ground zero (e.g. for benchmarking, etc.) @@ -67,80 +66,95 @@ using .CC: adce_pass!, JLOptions, verify_ir, verify_linetable using .EA: analyze_escapes, ArgEscapeCache, EscapeInfo, EscapeState, is_ipo_profitable +struct CodeCache + cache::IdDict{MethodInstance,CodeInstance} +end +CodeCache() = CodeCache(IdDict{MethodInstance,CodeInstance}()) +const GLOBAL_CODE_CACHE = CodeCache() + # when working outside of Core.Compiler, # cache entire escape state for later inspection and debugging -struct EscapeCache - cache::ArgEscapeCache +struct EscapeCacheInfo + argescapes::ArgEscapeCache state::EscapeState # preserved just for debugging purpose ir::IRCode # preserved just for debugging purpose end +struct EscapeCache + cache::IdDict{MethodInstance,EscapeCacheInfo} +end +EscapeCache() = EscapeCache(IdDict{MethodInstance,EscapeCacheInfo}()) +const GLOBAL_ESCAPE_CACHE = EscapeCache() + mutable struct EscapeAnalyzer <: AbstractInterpreter const world::UInt const inf_params::InferenceParams const opt_params::OptimizationParams const inf_cache::Vector{InferenceResult} - const cache::IdDict{InferenceResult,EscapeCache} + const code_cache::CodeCache + const escape_cache::EscapeCache const entry_tt - const optimize::Bool ir::IRCode - state::EscapeState + estate::EscapeState mi::MethodInstance - function EscapeAnalyzer(world::UInt, @nospecialize(tt), optimize::Bool) + function EscapeAnalyzer(world::UInt, @nospecialize(tt), + code_cache::CodeCache=GLOBAL_CODE_CACHE, + escape_cache::EscapeCache=GLOBAL_ESCAPE_CACHE) inf_params = InferenceParams() opt_params = OptimizationParams() inf_cache = InferenceResult[] - return new(world, inf_params, opt_params, inf_cache, IdDict{InferenceResult,EscapeCache}(), tt, optimize) + return new(world, inf_params, opt_params, inf_cache, code_cache, escape_cache, tt) end end -CC.InferenceParams(interp::EscapeAnalyzer) = interp.inf_params -CC.OptimizationParams(interp::EscapeAnalyzer) = interp.opt_params -CC.get_world_counter(interp::EscapeAnalyzer) = interp.world +CC.InferenceParams(interp::EscapeAnalyzer) = interp.inf_params +CC.OptimizationParams(interp::EscapeAnalyzer) = interp.opt_params +CC.get_world_counter(interp::EscapeAnalyzer) = interp.world CC.get_inference_cache(interp::EscapeAnalyzer) = interp.inf_cache -const GLOBAL_EA_CODE_CACHE = IdDict{MethodInstance,CodeInstance}() +struct EscapeAnalyzerCacheView + code_cache::CodeCache + escape_cache::EscapeCache +end function CC.code_cache(interp::EscapeAnalyzer) worlds = WorldRange(get_world_counter(interp)) - return WorldView(GlobalCache(), worlds) + return WorldView(EscapeAnalyzerCacheView(interp.code_cache, interp.escape_cache), worlds) end - -struct GlobalCache end - -CC.haskey(wvc::WorldView{GlobalCache}, mi::MethodInstance) = haskey(GLOBAL_EA_CODE_CACHE, mi) - -CC.get(wvc::WorldView{GlobalCache}, mi::MethodInstance, default) = get(GLOBAL_EA_CODE_CACHE, mi, default) - -CC.getindex(wvc::WorldView{GlobalCache}, mi::MethodInstance) = getindex(GLOBAL_EA_CODE_CACHE, mi) - -function CC.setindex!(wvc::WorldView{GlobalCache}, ci::CodeInstance, mi::MethodInstance) - GLOBAL_EA_CODE_CACHE[mi] = ci - add_callback!(mi) # register the callback on invalidation +CC.haskey(wvc::WorldView{EscapeAnalyzerCacheView}, mi::MethodInstance) = haskey(wvc.cache.code_cache.cache, mi) +CC.get(wvc::WorldView{EscapeAnalyzerCacheView}, mi::MethodInstance, default) = get(wvc.cache.code_cache.cache, mi, default) +CC.getindex(wvc::WorldView{EscapeAnalyzerCacheView}, mi::MethodInstance) = getindex(wvc.cache.code_cache.cache, mi) +function CC.setindex!(wvc::WorldView{EscapeAnalyzerCacheView}, ci::CodeInstance, mi::MethodInstance) + wvc.cache.code_cache.cache[mi] = ci + add_invalidation_callback!(wvc.cache.code_cache, wvc.cache.escape_cache, mi) # register the callback on invalidation return nothing end - -function add_callback!(mi) +function add_invalidation_callback!(code_cache::CodeCache, escape_cache::EscapeCache, mi) + callback = InvalidationCallback(code_cache, escape_cache) if !isdefined(mi, :callbacks) - mi.callbacks = Any[invalidate_cache!] + mi.callbacks = Any[callback] else - if !any(@nospecialize(cb)->cb===invalidate_cache!, mi.callbacks) - push!(mi.callbacks, invalidate_cache!) + if !any(@nospecialize(cb)->cb===callback, mi.callbacks) + push!(mi.callbacks, callback) end end return nothing end - -function invalidate_cache!(replaced, max_world, depth = 0) - delete!(GLOBAL_EA_CODE_CACHE, replaced) - +struct InvalidationCallback + code_cache::CodeCache + escape_cache::EscapeCache +end +function (callback::InvalidationCallback)(replaced::MethodInstance, max_world, + seen::IdSet{MethodInstance}=IdSet{MethodInstance}()) + (; code_cache, escape_cache) = callback + push!(seen, replaced) + delete!(code_cache.cache, replaced) + delete!(escape_cache.cache, replaced) if isdefined(replaced, :backedges) for mi in replaced.backedges - mi = mi::MethodInstance - if !haskey(GLOBAL_EA_CODE_CACHE, mi) - continue # otherwise fall into infinite loop - end - invalidate_cache!(mi, max_world, depth+1) + isa(mi, MethodInstance) || continue # might be `Type` object representing an `invoke` signature + mi in seen && continue # otherwise fall into infinite loop + callback(mi, max_world, seen) end end return nothing @@ -152,38 +166,26 @@ function CC.optimize(interp::EscapeAnalyzer, opt::OptimizationState, caller::Inf return CC.finish(interp, opt, ir, caller) end -function CC.cache_result!(interp::EscapeAnalyzer, caller::InferenceResult) - if haskey(interp.cache, caller) - GLOBAL_ESCAPE_CACHE[caller.linfo] = interp.cache[caller] - end - return @invoke CC.cache_result!(interp::AbstractInterpreter, caller::InferenceResult) -end - -const GLOBAL_ESCAPE_CACHE = IdDict{MethodInstance,EscapeCache}() - -""" - cache_escapes!(caller::InferenceResult, estate::EscapeState, cacheir::IRCode) - -Transforms escape information of call arguments of `caller`, -and then caches it into a global cache for later interprocedural propagation. -""" -function cache_escapes!(interp::EscapeAnalyzer, +function record_escapes!(interp::EscapeAnalyzer, caller::InferenceResult, estate::EscapeState, cacheir::IRCode) cache = ArgEscapeCache(estate) - ecache = EscapeCache(cache, estate, cacheir) - interp.cache[caller] = ecache - return cache + ecache = EscapeCacheInfo(cache, estate, cacheir) + return caller.argescapes = ecache end -function get_escape_cache(interp::EscapeAnalyzer) - return function (mi::Union{InferenceResult,MethodInstance}) - if isa(mi, InferenceResult) - ecache = get(interp.cache, mi, nothing) - else - ecache = get(GLOBAL_ESCAPE_CACHE, mi, nothing) - end - return ecache !== nothing ? ecache.cache : nothing - end +struct GetEscapeCache + escape_cache::EscapeCache + GetEscapeCache(interp::EscapeAnalyzer) = new(interp.escape_cache) +end +function ((; escape_cache)::GetEscapeCache)(mi::MethodInstance) + cached = get(escape_cache.cache, mi, nothing) + return cached === nothing ? nothing : cached.argescapes +end + +struct FailedAnalysis + ir::IRCode + nargs::Int + get_escape_cache::GetEscapeCache end function run_passes_ipo_safe_with_ea(interp::EscapeAnalyzer, @@ -192,51 +194,43 @@ function run_passes_ipo_safe_with_ea(interp::EscapeAnalyzer, @timeit "slot2reg" ir = slot2reg(ir, ci, sv) # TODO: Domsorting can produce an updated domtree - no need to recompute here @timeit "compact 1" ir = compact!(ir) - nargs = let def = sv.linfo.def; isa(def, Method) ? Int(def.nargs) : 0; end - local state - if is_ipo_profitable(ir, nargs) || caller.linfo.specTypes === interp.entry_tt - try - @timeit "[IPO EA]" begin - state = analyze_escapes(ir, nargs, false, get_escape_cache(interp)) - cache_escapes!(interp, caller, state, cccopy(ir)) - end - catch err - @error "error happened within [IPO EA], inspect `Main.ir` and `Main.nargs`" - @eval Main (ir = $ir; nargs = $nargs) - rethrow(err) - end - end - if caller.linfo.specTypes === interp.entry_tt && !interp.optimize - # return back the result - interp.ir = cccopy(ir) - interp.state = state - interp.mi = sv.linfo - end @timeit "Inlining" ir = ssa_inlining_pass!(ir, sv.inlining, ci.propagate_inbounds) # @timeit "verify 2" verify_ir(ir) @timeit "compact 2" ir = compact!(ir) - if caller.linfo.specTypes === interp.entry_tt && interp.optimize - try - @timeit "[Local EA]" state = analyze_escapes(ir, nargs, true, get_escape_cache(interp)) - catch err - @error "error happened within [Local EA], inspect `Main.ir` and `Main.nargs`" - @eval Main (ir = $ir; nargs = $nargs) - rethrow(err) - end - # return back the result - interp.ir = cccopy(ir) - interp.state = state - interp.mi = sv.linfo - end @timeit "SROA" ir = sroa_pass!(ir, sv.inlining) @timeit "ADCE" ir = adce_pass!(ir, sv.inlining) @timeit "compact 3" ir = compact!(ir, true) if JLOptions().debug_level == 2 @timeit "verify 3" (verify_ir(ir); verify_linetable(ir.linetable)) end + nargs = let def = sv.linfo.def; isa(def, Method) ? Int(def.nargs) : 0; end + get_escape_cache = GetEscapeCache(interp) + local estate::EscapeState + try + @timeit "EA" estate = analyze_escapes(ir, nargs, get_escape_cache) + catch err + @error "error happened within EA, inspect `Main.failed_escapeanalysis`" + @eval Main failed_escapeanalysis = $(FailedAnalysis(ir, nargs, get_escape_cache)) + rethrow(err) + end + if caller.linfo.specTypes === interp.entry_tt + # return back the result + interp.ir = cccopy(ir) + interp.estate = estate + interp.mi = sv.linfo + end + record_escapes!(interp, caller, estate, ir) return ir end +function CC.cache_result!(interp::EscapeAnalyzer, result::InferenceResult) + argescapes = result.argescapes + if argescapes isa EscapeCacheInfo + interp.escape_cache.cache[result.linfo] = argescapes + end + return @invoke CC.cache_result!(interp::AbstractInterpreter, result::InferenceResult) +end + # printing # -------- @@ -288,17 +282,19 @@ struct EscapeResult state::EscapeState mi::Union{Nothing,MethodInstance} source::Bool + interp::Union{Nothing,EscapeAnalyzer} function EscapeResult(ir::IRCode, state::EscapeState, - mi::Union{Nothing,MethodInstance} = nothing, - source::Bool=false) - return new(ir, state, mi, source) + mi::Union{Nothing,MethodInstance}=nothing, + source::Bool=false, + interp::Union{Nothing,EscapeAnalyzer}=nothing) + return new(ir, state, mi, source, interp) end end Base.show(io::IO, result::EscapeResult) = print_with_info(io, result) @eval Base.iterate(res::EscapeResult, state=1) = return state > $(fieldcount(EscapeResult)) ? nothing : (getfield(res, state), state+1) -Base.show(io::IO, cached::EscapeCache) = show(io, EscapeResult(cached.ir, cached.state, nothing)) +Base.show(io::IO, cached::EscapeCacheInfo) = show(io, EscapeResult(cached.ir, cached.state)) # adapted from https://github.com/JuliaDebug/LoweredCodeUtils.jl/blob/4612349432447e868cf9285f647108f43bd0a11c/src/codeedges.jl#L881-L897 function print_with_info(io::IO, (; ir, state, mi, source)::EscapeResult) diff --git a/test/compiler/EscapeAnalysis/local.jl b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl similarity index 90% rename from test/compiler/EscapeAnalysis/local.jl rename to test/compiler/EscapeAnalysis/EscapeAnalysis.jl index 1ca56b4dcd8c2..9dcfa591a54de 100644 --- a/test/compiler/EscapeAnalysis/local.jl +++ b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl @@ -1,10 +1,43 @@ -# Local EA Test -# ============= -# EA works on post-inlining IR +module test_EA -module test_local_EA +const use_core_compiler = false -include("setup.jl") +if use_core_compiler + const EscapeAnalysis = Core.Compiler.EscapeAnalysis +else + include(normpath(Sys.BINDIR, "..", "..", "base", "compiler", "ssair", "EscapeAnalysis", "EscapeAnalysis.jl")) +end + +include("EAUtils.jl") +include("../irutils.jl") + +using Test, .EscapeAnalysis, .EAUtils +using .EscapeAnalysis: ignore_argescape + +let utils_ex = quote + mutable struct SafeRef{T} + x::T + end + Base.getindex(s::SafeRef) = getfield(s, 1) + Base.setindex!(s::SafeRef, x) = setfield!(s, 1, x) + + mutable struct SafeRefs{S,T} + x1::S + x2::T + end + Base.getindex(s::SafeRefs, idx::Int) = getfield(s, idx) + Base.setindex!(s::SafeRefs, x, idx::Int) = setfield!(s, idx, x) + + global GV::Any + const global GR = Ref{Any}() + end + global function EATModule(utils_ex = utils_ex) + M = Module() + Core.eval(M, utils_ex) + return M + end + Core.eval(@__MODULE__, utils_ex) +end using Core.Compiler: alloc_array_ndims using .EscapeAnalysis: @@ -187,15 +220,6 @@ end end end -let # simple allocation - result = code_escapes((Bool,)) do c - mm = SafeRef{Bool}(c) # just allocated, never escapes - return mm[] ? nothing : 1 - end - i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_no_escape(result.state[SSAValue(i)]) -end - @testset "builtins" begin let # throw r = code_escapes((Any,)) do a @@ -683,8 +707,8 @@ end # field escape should propagate to :new arguments let result = code_escapes((String,)) do a o = SafeRef(a) - f = o[] - return f + Core.donotdelete(o) + return o[] end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) @@ -704,6 +728,7 @@ end end let result = code_escapes((String, String)) do a, b obj = SafeRefs(a, b) + Core.donotdelete(obj) fld1 = obj[1] fld2 = obj[2] return (fld1, fld2) @@ -718,9 +743,9 @@ end # field escape should propagate to `setfield!` argument let result = code_escapes((String,)) do a o = SafeRef("foo") + Core.donotdelete(o) o[] = a - f = o[] - return f + return o[] end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) @@ -730,6 +755,7 @@ end # propagate escape information imposed on return value of `setfield!` call let result = code_escapes((String,)) do a obj = SafeRef("foo") + Core.donotdelete(obj) return (obj[] = a) end i = only(findall(isnew, result.ir.stmts.stmt)) @@ -812,7 +838,9 @@ end end end let result = code_escapes((String,)) do x - broadcast(identity, Ref(x)) + o = Ref(x) + Core.donotdelete(o) + broadcast(identity, o) end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) @@ -882,6 +910,7 @@ end # alias via getfield & Expr(:new) let result = code_escapes((String,)) do s r = SafeRef(s) + Core.donotdelete(r) return r[] end i = only(findall(isnew, result.ir.stmts.stmt)) @@ -893,6 +922,7 @@ end let result = code_escapes((String,)) do s r1 = SafeRef(s) r2 = SafeRef(r1) + Core.donotdelete(r1, r2) return r2[] end i1, i2 = findall(isnew, result.ir.stmts.stmt) @@ -905,6 +935,7 @@ end let result = code_escapes((String,)) do s r1 = SafeRef(s) r2 = SafeRef(r1) + Core.donotdelete(r1, r2) return r2[][] end r = only(findall(isreturn, result.ir.stmts.stmt)) @@ -918,18 +949,20 @@ end const Rx = SafeRef("Rx") $code_escapes((String,)) do s r = SafeRef(Rx) + Core.donotdelete(r) rx = r[] # rx aliased to Rx rx[] = s nothing end end - i = findfirst(isnew, result.ir.stmts.stmt) + i = only(findall(isnew, result.ir.stmts.stmt)) @test has_all_escape(result.state[Argument(2)]) @test is_load_forwardable(result.state[SSAValue(i)]) end # alias via getfield & setfield! let result = code_escapes((String,)) do s r = Ref{String}() + Core.donotdelete(r) r[] = s return r[] end @@ -942,6 +975,7 @@ end let result = code_escapes((String,)) do s r1 = Ref(s) r2 = Ref{Base.RefValue{String}}() + Core.donotdelete(r1, r2) r2[] = r1 return r2[] end @@ -955,6 +989,7 @@ end let result = code_escapes((String,)) do s r1 = Ref{String}() r2 = Ref{Base.RefValue{String}}() + Core.donotdelete(r1, r2) r2[] = r1 r1[] = s return r2[][] @@ -983,6 +1018,7 @@ end const Rx = SafeRef("Rx") $code_escapes((SafeRef{String}, String,)) do _rx, s r = SafeRef(_rx) + Core.donotdelete(r) r[] = Rx rx = r[] # rx aliased to Rx rx[] = s @@ -1278,6 +1314,7 @@ end $code_escapes((String,)) do y x1 = SafeRef("init") x2 = SafeRef(y) + Core.donotdelete(x1, x2) setxy!(x1, x2[]) return x1 end @@ -1303,6 +1340,7 @@ end let result = code_escapes((Any,Any)) do a, b r = SafeRef{Any}(a) + Core.donotdelete(r) r[] = b return r[] end @@ -1314,6 +1352,7 @@ end end let result = code_escapes((Any,Any)) do a, b r = SafeRef{Any}(:init) + Core.donotdelete(r) r[] = a r[] = b return r[] @@ -1326,6 +1365,7 @@ end end let result = code_escapes((Any,Any,Bool)) do a, b, cond r = SafeRef{Any}(:init) + Core.donotdelete(r) if cond r[] = a return r[] @@ -1432,15 +1472,15 @@ function compute(a, b) end a.x, a.y end -let result = @code_escapes compute(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) - idxs = findall(1:length(result.ir.stmts)) do idx - inst = result.ir[SSAValue(idx)] - stmt = inst[:stmt] - return isnew(stmt) && inst[:type] <: MPoint - end - @assert length(idxs) == 2 - @test count(i->is_load_forwardable(result.state[SSAValue(i)]), idxs) == 1 -end +# let result = @code_escapes compute(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) +# idxs = findall(1:length(result.ir.stmts)) do idx +# inst = result.ir[SSAValue(idx)] +# stmt = inst[:stmt] +# return isnew(stmt) && inst[:type] <: MPoint +# end +# @assert length(idxs) == 2 +# @test count(i->is_load_forwardable(result.state[SSAValue(i)]), idxs) == 1 +# end function compute!(a, b) for i in 0:(100000000-1) c = add(a, b) # replaceable @@ -1460,8 +1500,6 @@ let result = @code_escapes compute!(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.7 end @testset "array primitives" begin - inbounds = Base.JLOptions().check_bounds == 0 - # arrayref let result = code_escapes((Vector{String},Int)) do xs, i s = Base.arrayref(true, xs, i) @@ -1481,15 +1519,6 @@ end @test !has_thrown_escape(result.state[Argument(2)]) # xs @test !has_return_escape(result.state[Argument(3)], r) # i end - inbounds && let result = code_escapes((Vector{String},Int)) do xs, i - s = @inbounds xs[i] - return s - end - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # xs - @test !has_thrown_escape(result.state[Argument(2)]) # xs - @test !has_return_escape(result.state[Argument(3)], r) # i - end let result = code_escapes((Vector{String},Bool)) do xs, i c = Base.arrayref(true, xs, i) # TypeError will happen here return c @@ -1538,15 +1567,6 @@ end @test !has_thrown_escape(result.state[Argument(2)]) # xs @test has_return_escape(result.state[Argument(3)], r) # x end - inbounds && let result = code_escapes((Vector{String},String,Int,)) do xs, x, i - @inbounds xs[i] = x - return xs - end - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # xs - @test !has_thrown_escape(result.state[Argument(2)]) # xs - @test has_return_escape(result.state[Argument(3)], r) # x - end let result = code_escapes((String,String,String,)) do s, t, u xs = Vector{String}(undef, 3) Base.arrayset(true, xs, s, 1) @@ -1739,15 +1759,6 @@ end @test has_thrown_escape(result.state[SSAValue(i)], t) # xs @test has_thrown_escape(result.state[Argument(2)], t) # x end - inbounds && let result = code_escapes((String,)) do x - xs = @inbounds Any[x] - @ccall jl_array_del_at(xs::Any, 1::UInt, 2::UInt)::Cvoid # can potentially throw - end - i = only(findall(isarrayalloc, result.ir.stmts.stmt)) - t = only(findall(isarrayresize, result.ir.stmts.stmt)) - @test has_thrown_escape(result.state[SSAValue(i)], t) # xs - @test has_thrown_escape(result.state[Argument(2)], t) # x - end # array copy let result = code_escapes((Vector{Any},)) do xs @@ -2201,8 +2212,8 @@ end # @static if isdefined(Core, :ImmutableArray) let result = code_escapes((Int,)) do a o = SafeRef(a) - f = o[] - return f + Core.donotdelete(o) + return o[] end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) @@ -2240,4 +2251,162 @@ end # return m # end -end # module test_local_EA +# interprocedural analysis +# ------------------------ + +# propagate escapes imposed on call arguments +@noinline broadcast_noescape1(a) = (broadcast(identity, a); nothing) +let result = code_escapes() do + broadcast_noescape1(Ref("Hi")) + end + i = only(findall(isnew, result.ir.stmts.stmt)) + @test !has_return_escape(result.state[SSAValue(i)]) + @test_broken !has_thrown_escape(result.state[SSAValue(i)]) # TODO `getfield(RefValue{String}, :x)` isn't safe +end +@noinline broadcast_noescape2(b) = broadcast(identity, b) +let result = code_escapes() do + broadcast_noescape2(Ref("Hi")) + end + i = only(findall(isnew, result.ir.stmts.stmt)) + @test_broken !has_return_escape(result.state[SSAValue(i)]) # TODO interprocedural alias analysis + @test_broken !has_thrown_escape(result.state[SSAValue(i)]) # TODO `getfield(RefValue{String}, :x)` isn't safe +end +@noinline allescape_argument(a) = (global GV = a) # obvious escape +let result = code_escapes() do + allescape_argument(Ref("Hi")) + end + i = only(findall(isnew, result.ir.stmts.stmt)) + @test has_all_escape(result.state[SSAValue(i)]) +end +# if we can't determine the matching method statically, we should be conservative +let result = code_escapes((Ref{Any},)) do a + may_exist(a) + end + @test has_all_escape(result.state[Argument(2)]) +end +let result = code_escapes((Ref{Any},)) do a + Base.@invokelatest broadcast_noescape1(a) + end + @test has_all_escape(result.state[Argument(2)]) +end + +# handling of simple union-split (just exploit the inliner's effort) +@noinline unionsplit_noescape(a) = string(nothing) +@noinline unionsplit_noescape(a::Int) = a + 10 +let result = code_escapes((Union{Int,Nothing},)) do x + s = SafeRef{Union{Int,Nothing}}(x) + unionsplit_noescape(s[]) + return nothing + end + inds = findall(isnew, result.ir.stmts.stmt) # find allocation statement + @assert !isempty(inds) + for i in inds + @test has_no_escape(result.state[SSAValue(i)]) + end +end + +@noinline unused_argument(a) = (println("prevent inlining"); nothing) +let result = code_escapes() do + a = Ref("foo") # shouldn't be "return escape" + b = unused_argument(a) + nothing + end + i = only(findall(isnew, result.ir.stmts.stmt)) + @test has_no_escape(result.state[SSAValue(i)]) + + result = code_escapes() do + a = Ref("foo") # still should be "return escape" + b = unused_argument(a) + return a + end + i = only(findall(isnew, result.ir.stmts.stmt)) + r = only(findall(isreturn, result.ir.stmts.stmt)) + @test has_return_escape(result.state[SSAValue(i)], r) +end + +# should propagate escape information imposed on return value to the aliased call argument +@noinline returnescape_argument(a) = (println("prevent inlining"); a) +let result = code_escapes() do + obj = Ref("foo") # should be "return escape" + ret = returnescape_argument(obj) + return ret # alias of `obj` + end + i = only(findall(isnew, result.ir.stmts.stmt)) + r = only(findall(isreturn, result.ir.stmts.stmt)) + @test has_return_escape(result.state[SSAValue(i)], r) +end +@noinline noreturnescape_argument(a) = (println("prevent inlining"); identity("hi")) +let result = code_escapes() do + obj = Ref("foo") # better to not be "return escape" + ret = noreturnescape_argument(obj) + return ret # must not alias to `obj` + end + i = only(findall(isnew, result.ir.stmts.stmt)) + @test has_no_escape(result.state[SSAValue(i)]) +end + +# accounts for ThrownEscape via potential MethodError + +# no method error +@noinline identity_if_string(x::SafeRef) = (println("preventing inlining"); nothing) +let result = code_escapes((SafeRef{String},)) do x + Base.donotdelete(1) # TODO #51143 + identity_if_string(x) + end + @test has_no_escape(ignore_argescape(result.state[Argument(2)])) +end +let result = code_escapes((Union{SafeRef{String},Nothing},)) do x + Base.donotdelete(1) # TODO #51143 + identity_if_string(x) + end + i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.stmt)) + r = only(findall(isreturn, result.ir.stmts.stmt)) + @test has_thrown_escape(result.state[Argument(2)], i) + @test_broken !has_return_escape(result.state[Argument(2)], r) +end +let result = code_escapes((SafeRef{String},)) do x + try + identity_if_string(x) + catch err + global GV = err + end + return nothing + end + @test !has_all_escape(result.state[Argument(2)]) +end +let result = code_escapes((Union{SafeRef{String},Vector{String}},)) do x + try + identity_if_string(x) + catch err + global GV = err + end + return nothing + end + @test has_all_escape(result.state[Argument(2)]) +end +# method ambiguity error +@noinline ambig_error_test(a::SafeRef, b) = (println("preventing inlining"); nothing) +@noinline ambig_error_test(a, b::SafeRef) = (println("preventing inlining"); nothing) +@noinline ambig_error_test(a, b) = (println("preventing inlining"); nothing) +let result = code_escapes((SafeRef{String},Any)) do x, y + ambig_error_test(x, y) + end + i = only(findall(iscall((result.ir, ambig_error_test)), result.ir.stmts.stmt)) + r = only(findall(isreturn, result.ir.stmts.stmt)) + @test has_thrown_escape(result.state[Argument(2)], i) # x + @test has_thrown_escape(result.state[Argument(3)], i) # y + @test_broken !has_return_escape(result.state[Argument(2)], r) # x + @test_broken !has_return_escape(result.state[Argument(3)], r) # y +end +let result = code_escapes((SafeRef{String},Any)) do x, y + try + ambig_error_test(x, y) + catch err + global GV = err + end + end + @test has_all_escape(result.state[Argument(2)]) # x + @test has_all_escape(result.state[Argument(3)]) # y +end + +end # module test_EA diff --git a/test/compiler/EscapeAnalysis/interprocedural.jl b/test/compiler/EscapeAnalysis/interprocedural.jl deleted file mode 100644 index 6985ce206bd83..0000000000000 --- a/test/compiler/EscapeAnalysis/interprocedural.jl +++ /dev/null @@ -1,275 +0,0 @@ -# IPO EA Test -# =========== -# EA works on pre-inlining IR - -module test_IPO_EA - -include("setup.jl") - -# callsites -# --------- - -noescape(a) = nothing -noescape(a, b) = nothing -function global_escape!(x) - GR[] = x - return nothing -end -union_escape!(x) = global_escape!(x) -union_escape!(x::SafeRef) = nothing -union_escape!(x::SafeRefs) = nothing -Base.@constprop :aggressive function conditional_escape!(cnd, x) - cnd && global_escape!(x) - return nothing -end - -# MethodMatchInfo -- global cache -let result = code_escapes((SafeRef{String},); optimize=false) do x - # TODO: This is to prevent this function from having ConstABI, - # which currently breaks EA (xref, #51143) - Base.donotdelete(1) - return noescape(x) - end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - identity(x) - return nothing - end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - return identity(x) - end - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - return Ref(x) - end - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - r = Ref{SafeRef{String}}() - r[] = x - return r - end - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - global_escape!(x) - end - @test has_all_escape(result.state[Argument(2)]) -end -# UnionSplitInfo -let result = code_escapes((Bool,Vector{Any}); optimize=false) do c, s - x = c ? s : SafeRef(s) - union_escape!(x) - end - @test has_all_escape(result.state[Argument(3)]) # s -end -let result = code_escapes((Bool,Vector{Any}); optimize=false) do c, s - Base.donotdelete(1) # TODO #51143 - x = c ? SafeRef(s) : SafeRefs(s, s) - union_escape!(x) - end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) -end -# ConstCallInfo -- local cache -let result = code_escapes((SafeRef{String},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - return conditional_escape!(false, x) - end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) -end -# InvokeCallInfo -let result = code_escapes((SafeRef{String},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - return @invoke noescape(x::Any) - end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - return @invoke conditional_escape!(false::Any, x::Any) - end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) -end - -# MethodError -# ----------- -# accounts for ThrownEscape via potential MethodError - -# no method error -identity_if_string(x::SafeRef) = nothing -let result = code_escapes((SafeRef{String},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - identity_if_string(x) - end - @test !has_thrown_escape(result.state[Argument(2)]) - @test !has_return_escape(result.state[Argument(2)]) -end -let result = code_escapes((Union{SafeRef{String},Vector{String}},); optimize=false) do x - Base.donotdelete(1) # TODO #51143 - identity_if_string(x) - end - i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.stmt)) - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_thrown_escape(result.state[Argument(2)], i) - @test_broken !has_return_escape(result.state[Argument(2)], r) -end -let result = code_escapes((SafeRef{String},); optimize=false) do x - try - identity_if_string(x) - catch err - global GV = err - end - return nothing - end - @test !has_all_escape(result.state[Argument(2)]) -end -let result = code_escapes((Union{SafeRef{String},Vector{String}},); optimize=false) do x - try - identity_if_string(x) - catch err - global GV = err - end - return nothing - end - @test has_all_escape(result.state[Argument(2)]) -end -# method ambiguity error -ambig_error_test(a::SafeRef, b) = nothing -ambig_error_test(a, b::SafeRef) = nothing -ambig_error_test(a, b) = nothing -let result = code_escapes((SafeRef{String},Any); optimize=false) do x, y - ambig_error_test(x, y) - end - i = only(findall(iscall((result.ir, ambig_error_test)), result.ir.stmts.stmt)) - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_thrown_escape(result.state[Argument(2)], i) # x - @test has_thrown_escape(result.state[Argument(3)], i) # y - @test_broken !has_return_escape(result.state[Argument(2)], r) # x - @test_broken !has_return_escape(result.state[Argument(3)], r) # y -end -let result = code_escapes((SafeRef{String},Any); optimize=false) do x, y - try - ambig_error_test(x, y) - catch err - global GV = err - end - end - @test has_all_escape(result.state[Argument(2)]) # x - @test has_all_escape(result.state[Argument(3)]) # y -end - -# Local EA integration -# -------------------- - -# propagate escapes imposed on call arguments - -# FIXME handle _apply_iterate -# FIXME currently we can't prove the effect-freeness of `getfield(RefValue{String}, :x)` -# because of this check https://github.com/JuliaLang/julia/blob/94b9d66b10e8e3ebdb268e4be5f7e1f43079ad4e/base/compiler/tfuncs.jl#L745 -# and thus it leads to the following two broken tests - -@noinline broadcast_noescape1(a) = (broadcast(identity, a); nothing) -let result = code_escapes() do - broadcast_noescape1(Ref("Hi")) - end - i = only(findall(isnew, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)]) - @test_broken !has_thrown_escape(result.state[SSAValue(i)]) -end -@noinline broadcast_noescape2(b) = broadcast(identity, b) -let result = code_escapes() do - broadcast_noescape2(Ref("Hi")) - end - i = only(findall(isnew, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)]) - @test_broken !has_thrown_escape(result.state[SSAValue(i)]) -end -@noinline allescape_argument(a) = (global GV = a) # obvious escape -let result = code_escapes() do - allescape_argument(Ref("Hi")) - end - i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) -end -# if we can't determine the matching method statically, we should be conservative -let result = code_escapes((Ref{Any},)) do a - may_exist(a) - end - @test has_all_escape(result.state[Argument(2)]) -end -let result = code_escapes((Ref{Any},)) do a - Base.@invokelatest broadcast_noescape1(a) - end - @test has_all_escape(result.state[Argument(2)]) -end - -# handling of simple union-split (just exploit the inliner's effort) -@noinline unionsplit_noescape(a) = string(nothing) -@noinline unionsplit_noescape(a::Int) = a + 10 -let result = code_escapes((Union{Int,Nothing},)) do x - s = SafeRef{Union{Int,Nothing}}(x) - unionsplit_noescape(s[]) - return nothing - end - inds = findall(isnew, result.ir.stmts.stmt) # find allocation statement - @assert !isempty(inds) - for i in inds - @test has_no_escape(result.state[SSAValue(i)]) - end -end - -@noinline function unused_argument(a) - println("prevent inlining") - return Base.inferencebarrier(nothing) -end -let result = code_escapes() do - a = Ref("foo") # shouldn't be "return escape" - b = unused_argument(a) - nothing - end - i = only(findall(isnew, result.ir.stmts.stmt)) - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test !has_return_escape(result.state[SSAValue(i)], r) - - result = code_escapes() do - a = Ref("foo") # still should be "return escape" - b = unused_argument(a) - return a - end - i = only(findall(isnew, result.ir.stmts.stmt)) - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) -end - -# should propagate escape information imposed on return value to the aliased call argument -@noinline returnescape_argument(a) = (println("prevent inlining"); a) -let result = code_escapes() do - obj = Ref("foo") # should be "return escape" - ret = returnescape_argument(obj) - return ret # alias of `obj` - end - i = only(findall(isnew, result.ir.stmts.stmt)) - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) -end -@noinline noreturnescape_argument(a) = (println("prevent inlining"); identity("hi")) -let result = code_escapes() do - obj = Ref("foo") # better to not be "return escape" - ret = noreturnescape_argument(obj) - return ret # must not alias to `obj` - end - i = only(findall(isnew, result.ir.stmts.stmt)) - r = only(findall(isreturn, result.ir.stmts.stmt)) - @test !has_return_escape(result.state[SSAValue(i)], r) -end - -end # module test_IPO_EA diff --git a/test/compiler/EscapeAnalysis/setup.jl b/test/compiler/EscapeAnalysis/setup.jl deleted file mode 100644 index 56d56ac0c89e7..0000000000000 --- a/test/compiler/EscapeAnalysis/setup.jl +++ /dev/null @@ -1,38 +0,0 @@ -const use_core_compiler = true - -if use_core_compiler - const EscapeAnalysis = Core.Compiler.EscapeAnalysis -else - include(normpath(Sys.BINDIR, "..", "..", "base", "compiler", "ssair", "EscapeAnalysis", "EscapeAnalysis.jl")) -end - -include("EAUtils.jl") -include("../irutils.jl") - -using Test, .EscapeAnalysis, .EAUtils -using .EscapeAnalysis: ignore_argescape - -let setup_ex = quote - mutable struct SafeRef{T} - x::T - end - Base.getindex(s::SafeRef) = getfield(s, 1) - Base.setindex!(s::SafeRef, x) = setfield!(s, 1, x) - - mutable struct SafeRefs{S,T} - x1::S - x2::T - end - Base.getindex(s::SafeRefs, idx::Int) = getfield(s, idx) - Base.setindex!(s::SafeRefs, x, idx::Int) = setfield!(s, idx, x) - - global GV::Any - const global GR = Ref{Any}() - end - global function EATModule(setup_ex = setup_ex) - M = Module() - Core.eval(M, setup_ex) - return M - end - Core.eval(@__MODULE__, setup_ex) -end