From b19a7c1721f623ae085354889b183622537543b0 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:13:08 +0900 Subject: [PATCH] optimizer: allow EA-powered `finalizer` inlining (#55954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #3 if not %4 2 ── invoke Core.throw_inexacterror(:convert::Symbol, UInt64::Type, %1::Int64)::Union{} └─── unreachable 3 ── goto #4 4 ── %9 = Core.bitcast(Core.UInt64, %1)::UInt64 └─── goto #5 5 ── goto #6 6 ── goto #7 7 ── goto #8 8 ── %14 = $(Expr(:foreigncall, :(:malloc), Ptr{Nothing}, svec(UInt64), 0, :(:ccall), :(%9), :(%9)))::Ptr{Nothing} └─── goto #9 9 ── %16 = Base.bitcast(Ptr{Float64}, %14)::Ptr{Float64} │ %17 = %new(ForeignBuffer{Float64}, %16)::ForeignBuffer{Float64} └─── goto #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/julia#55990). --- base/compiler/optimize.jl | 2 +- .../ssair/EscapeAnalysis/EscapeAnalysis.jl | 59 +++++--- base/compiler/ssair/passes.jl | 86 +++++++---- base/compiler/types.jl | 2 + test/compiler/EscapeAnalysis/EAUtils.jl | 142 +++++++++++------- test/compiler/codegen.jl | 2 +- test/compiler/inline.jl | 33 ++++ 7 files changed, 225 insertions(+), 101 deletions(-) diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 5f0c5077688f8..c5606f80468c0 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -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) diff --git a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl index a0abacb617085..1f98758cd6055 100644 --- a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl +++ b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl @@ -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!, @@ -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) @@ -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) @@ -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}) @@ -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 @@ -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 diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 0e2272524a0ed..e3f294c4e91fe 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -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) @@ -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 @@ -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)] diff --git a/base/compiler/types.jl b/base/compiler/types.jl index 210adf7be96b2..b6c976da48f67 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -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 diff --git a/test/compiler/EscapeAnalysis/EAUtils.jl b/test/compiler/EscapeAnalysis/EAUtils.jl index c41e61e231892..1f0a84f1a8365 100644 --- a/test/compiler/EscapeAnalysis/EAUtils.jl +++ b/test/compiler/EscapeAnalysis/EAUtils.jl @@ -6,56 +6,6 @@ const CC = Core.Compiler using ..EscapeAnalysis const EA = EscapeAnalysis -# entries -# ------- - -using Base: IdSet, unwrap_unionall, rewrap_unionall -using InteractiveUtils: gen_call_with_extracted_types_and_kwargs - -""" - @code_escapes [options...] f(args...) - -Evaluates the arguments to the function call, determines its types, and then calls -[`code_escapes`](@ref) on the resulting expression. -As with `@code_typed` and its family, any of `code_escapes` keyword arguments can be given -as the optional arguments like `@code_escapes optimize=false myfunc(myargs...)`. -""" -macro code_escapes(ex0...) - return gen_call_with_extracted_types_and_kwargs(__module__, :code_escapes, ex0) -end - -""" - code_escapes(f, argtypes=Tuple{}; [debuginfo::Symbol = :none], [optimize::Bool = true]) -> result::EscapeResult - -Runs the escape analysis on optimized IR of a generic function call with the given type signature. - -# Keyword Arguments - -- `optimize::Bool = true`: - if `true` returns escape information of post-inlining IR (used for local optimization), - otherwise returns escape information of pre-inlining IR (used for interprocedural escape information generation) -- `debuginfo::Symbol = :none`: - controls the amount of code metadata present in the output, possible options are `:none` or `:source`. -""" -function code_escapes(@nospecialize(f), @nospecialize(types=Base.default_tt(f)); - world::UInt = get_world_counter(), - debuginfo::Symbol = :none) - tt = Base.signature_type(f, types) - match = Base._which(tt; world, raise=true) - mi = Core.Compiler.specialize_method(match)::MethodInstance - interp = EscapeAnalyzer(world, mi) - frame = Core.Compiler.typeinf_frame(interp, mi, #=run_optimizer=#true) - isdefined(interp, :result) || error("optimization didn't happen: maybe everything has been constant folded?") - slotnames = let src = frame.src - src isa CodeInfo ? src.slotnames : nothing - end - return EscapeResult(interp.result.ir, interp.result.estate, interp.result.mi, - slotnames, debuginfo === :source, interp) -end - -# in order to run a whole analysis from ground zero (e.g. for benchmarking, etc.) -__clear_cache!() = empty!(GLOBAL_EA_CODE_CACHE) - # AbstractInterpreter # ------------------- @@ -99,10 +49,10 @@ mutable struct EscapeAnalyzer <: AbstractInterpreter const opt_params::OptimizationParams const inf_cache::Vector{InferenceResult} const escape_cache::EscapeCache - const entry_mi::MethodInstance + const entry_mi::Union{Nothing,MethodInstance} result::EscapeResultForEntry - function EscapeAnalyzer(world::UInt, entry_mi::MethodInstance, - escape_cache::EscapeCache=GLOBAL_ESCAPE_CACHE) + function EscapeAnalyzer(world::UInt, escape_cache::EscapeCache; + entry_mi::Union{Nothing,MethodInstance}=nothing) inf_params = InferenceParams() opt_params = OptimizationParams() inf_cache = InferenceResult[] @@ -115,6 +65,7 @@ CC.OptimizationParams(interp::EscapeAnalyzer) = interp.opt_params CC.get_inference_world(interp::EscapeAnalyzer) = interp.world CC.get_inference_cache(interp::EscapeAnalyzer) = interp.inf_cache CC.cache_owner(::EscapeAnalyzer) = EAToken() +CC.get_escape_cache(interp::EscapeAnalyzer) = GetEscapeCache(interp) function CC.ipo_dataflow_analysis!(interp::EscapeAnalyzer, opt::OptimizationState, ir::IRCode, caller::InferenceResult) @@ -125,8 +76,9 @@ function CC.ipo_dataflow_analysis!(interp::EscapeAnalyzer, opt::OptimizationStat estate = try analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache) catch err - @error "error happened within EA, inspect `Main.failed_escapeanalysis`" - Main.failed_escapeanalysis = FailedAnalysis(ir, nargs, get_escape_cache) + @error "error happened within EA, inspect `Main.failedanalysis`" + failedanalysis = FailedAnalysis(caller, ir, nargs, get_escape_cache) + Core.eval(Main, :(failedanalysis = $failedanalysis)) rethrow(err) end if caller.linfo === interp.entry_mi @@ -156,6 +108,7 @@ function ((; escape_cache)::GetEscapeCache)(mi::MethodInstance) end struct FailedAnalysis + caller::InferenceResult ir::IRCode nargs::Int get_escape_cache::GetEscapeCache @@ -301,4 +254,83 @@ function print_with_info(preprint, postprint, io::IO, ir::IRCode, source::Bool) return nothing end +# entries +# ------- + +using InteractiveUtils: gen_call_with_extracted_types_and_kwargs + +""" + @code_escapes [options...] f(args...) + +Evaluates the arguments to the function call, determines its types, and then calls +[`code_escapes`](@ref) on the resulting expression. +As with `@code_typed` and its family, any of `code_escapes` keyword arguments can be given +as the optional arguments like `@code_escapes optimize=false myfunc(myargs...)`. +""" +macro code_escapes(ex0...) + return gen_call_with_extracted_types_and_kwargs(__module__, :code_escapes, ex0) +end + +""" + code_escapes(f, argtypes=Tuple{}; [world::UInt], [debuginfo::Symbol]) -> result::EscapeResult + code_escapes(mi::MethodInstance; [world::UInt], [interp::EscapeAnalyzer], [debuginfo::Symbol]) -> result::EscapeResult + +Runs the escape analysis on optimized IR of a generic function call with the given type signature, +while caching the analysis results. + +# Keyword Arguments + +- `world::UInt = Base.get_world_counter()`: + controls the world age to use when looking up methods, use current world age if not specified. +- `interp::EscapeAnalyzer = EscapeAnalyzer(world)`: + specifies the escape analyzer to use, by default a new analyzer with the global cache is created. +- `debuginfo::Symbol = :none`: + controls the amount of code metadata present in the output, possible options are `:none` or `:source`. +""" +function code_escapes(@nospecialize(f), @nospecialize(types=Base.default_tt(f)); + world::UInt = get_world_counter(), + debuginfo::Symbol = :none) + tt = Base.signature_type(f, types) + match = Base._which(tt; world, raise=true) + mi = Core.Compiler.specialize_method(match) + return code_escapes(mi; world, debuginfo) +end + +function code_escapes(mi::MethodInstance; + world::UInt = get_world_counter(), + interp::EscapeAnalyzer=EscapeAnalyzer(world, GLOBAL_ESCAPE_CACHE; entry_mi=mi), + debuginfo::Symbol = :none) + frame = Core.Compiler.typeinf_frame(interp, mi, #=run_optimizer=#true) + isdefined(interp, :result) || error("optimization didn't happen: maybe everything has been constant folded?") + slotnames = let src = frame.src + src isa CodeInfo ? src.slotnames : nothing + end + return EscapeResult(interp.result.ir, interp.result.estate, interp.result.mi, + slotnames, debuginfo === :source, interp) +end + +""" + code_escapes(ir::IRCode, nargs::Int; [world::UInt], [interp::AbstractInterpreter]) -> result::EscapeResult + +Runs the escape analysis on `ir::IRCode`. +`ir` is supposed to be optimized already, specifically after inlining has been applied. +Note that this version does not cache the analysis results. + +# Keyword Arguments + +- `world::UInt = Base.get_world_counter()`: + controls the world age to use when looking up methods, use current world age if not specified. +- `interp::AbstractInterpreter = EscapeAnalyzer(world, EscapeCache())`: + specifies the abstract interpreter to use, by default a new `EscapeAnalyzer` with an empty cache is created. +""" +function code_escapes(ir::IRCode, nargs::Int; + world::UInt = get_world_counter(), + interp::AbstractInterpreter=EscapeAnalyzer(world, EscapeCache())) + estate = analyze_escapes(ir, nargs, CC.optimizer_lattice(interp), CC.get_escape_cache(interp)) + return EscapeResult(ir, estate) # return back the result +end + +# in order to run a whole analysis from ground zero (e.g. for benchmarking, etc.) +__clear_cache!() = empty!(GLOBAL_EA_CODE_CACHE) + end # module EAUtils diff --git a/test/compiler/codegen.jl b/test/compiler/codegen.jl index 26ae965b35319..ae04250964554 100644 --- a/test/compiler/codegen.jl +++ b/test/compiler/codegen.jl @@ -266,7 +266,7 @@ if opt_level > 0 @test occursin("ret $Iptr %\"x::$(Int)\"", load_dummy_ref_ir) end -# Issue 22770 +# Issue JuliaLang/julia#22770 let was_gced = false @noinline make_tuple(x) = tuple(x) @noinline use(x) = ccall(:jl_breakpoint, Cvoid, ()) diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 99fed00a7269d..53f7adc2a2a77 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -2249,3 +2249,36 @@ let src = code_typed1(bar_split_error, Tuple{}) @test count(iscall((src, foo_split)), src.code) == 0 @test count(iscall((src, Core.throw_methoderror)), src.code) > 0 end + +# finalizer inlining with EA +mutable struct ForeignBuffer{T} + const ptr::Ptr{T} +end +mutable struct ForeignBufferChecker + @atomic finalized::Bool +end +const foreign_buffer_checker = ForeignBufferChecker(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 + @atomic foreign_buffer_checker.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 +let src = code_typed1(f_EA_finalizer, (Int,)) + @test count(iscall((src, Core.finalizer)), src.code) == 0 +end +let;Base.Experimental.@force_compile + f_EA_finalizer(42000) + @test foreign_buffer_checker.finalized +end