Skip to content

Commit

Permalink
Eager finalizer insertion (#45272)
Browse files Browse the repository at this point in the history
* Eager finalizer insertion

This is a variant of the eager-finalization idea
(e.g. as seen in #44056), but with a focus on the mechanism
of finalizer insertion, since I need a similar pass downstream.
Integration of EscapeAnalysis is left to #44056.

My motivation for this change is somewhat different. In particular,
I want to be able to insert finalize call such that I can
subsequently SROA the mutable object. This requires a couple
design points that are more stringent than the pass from #44056,
so I decided to prototype them as an independent PR. The primary
things I need here that are not seen in #44056 are:

- The ability to forgo finalizer registration with the runtime
  entirely (requires additional legality analyis)
- The ability to inline the registered finalizer at the deallocation
  point (to enable subsequent SROA)

To this end, adding a finalizer is promoted to a builtin
that is recognized by inference and inlining (such that inference
can produce an inferred version of the finalizer for inlining).

The current status is that this fixes the minimal example I wanted
to have work, but does not yet extend to the motivating case I had.
Nevertheless, I felt that this was a good checkpoint to synchronize
with other efforts along these lines.

Currently working demo:

```
julia> const total_deallocations = Ref{Int}(0)
Base.RefValue{Int64}(0)

julia> mutable struct DoAlloc
               function DoAlloc()
                   this = new()
                       Core._add_finalizer(this, function(this)
                               global total_deallocations[] += 1
                       end)
                       return this
               end
       end

julia> function foo()
               for i = 1:1000
                       DoAlloc()
               end
       end
foo (generic function with 1 method)

julia> @code_llvm foo()
;  @ REPL[3]:1 within `foo`
define void @julia_foo_111() #0 {
top:
  %.promoted = load i64, i64* inttoptr (i64 140370001753968 to i64*), align 16
;  @ REPL[3]:2 within `foo`
  %0 = add i64 %.promoted, 1000
;  @ REPL[3] within `foo`
  store i64 %0, i64* inttoptr (i64 140370001753968 to i64*), align 16
;  @ REPL[3]:4 within `foo`
  ret void
}
```

* rm redundant copy

Co-authored-by: Shuhei Kadowaki <[email protected]>
  • Loading branch information
Keno and aviatesk authored Jun 7, 2022
1 parent 9086fd0 commit c4effda
Show file tree
Hide file tree
Showing 9 changed files with 382 additions and 79 deletions.
11 changes: 11 additions & 0 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,15 @@ function invoke_rewrite(xs::Vector{Any})
return newxs
end

function abstract_finalizer(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState)
if length(argtypes) == 3
finalizer_argvec = Any[argtypes[2], argtypes[3]]
call = abstract_call(interp, ArgInfo(nothing, finalizer_argvec), sv, 1)
return CallMeta(Nothing, Effects(), FinalizerInfo(call.info, call.effects))
end
return CallMeta(Nothing, Effects(), false)
end

# call where the function is known exactly
function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
arginfo::ArgInfo, sv::InferenceState,
Expand All @@ -1633,6 +1642,8 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
return abstract_invoke(interp, arginfo, sv)
elseif f === modifyfield!
return abstract_modifyfield!(interp, argtypes, sv)
elseif f === Core.finalizer
return abstract_finalizer(interp, argtypes, sv)
end
rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods)
return CallMeta(rt, builtin_effects(f, argtypes, rt), false)
Expand Down
5 changes: 4 additions & 1 deletion base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const IR_FLAG_THROW_BLOCK = 0x01 << 3
# This statement may be removed if its result is unused. In particular it must
# thus be both pure and effect free.
const IR_FLAG_EFFECT_FREE = 0x01 << 4
# This statement was proven not to throw
const IR_FLAG_NOTHROW = 0x01 << 5


const TOP_TUPLE = GlobalRef(Core, :tuple)

Expand Down Expand Up @@ -567,7 +570,7 @@ function run_passes(
@pass "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds)
# @timeit "verify 2" verify_ir(ir)
@pass "compact 2" ir = compact!(ir)
@pass "SROA" ir = sroa_pass!(ir)
@pass "SROA" ir = sroa_pass!(ir, sv.inlining)
@pass "ADCE" ir = adce_pass!(ir)
@pass "type lift" ir = type_lift_pass!(ir)
@pass "compact 3" ir = compact!(ir)
Expand Down
134 changes: 98 additions & 36 deletions base/compiler/ssair/inlining.jl
Original file line number Diff line number Diff line change
Expand Up @@ -308,21 +308,17 @@ function finish_cfg_inline!(state::CFGInliningState)
end
end

function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any},
linetable::Vector{LineInfoNode}, item::InliningTodo,
boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}})
# Ok, do the inlining here
spec = item.spec::ResolvedInliningSpec
sparam_vals = item.mi.sparam_vals
def = item.mi.def::Method
function ir_inline_linetable!(linetable::Vector{LineInfoNode}, inlinee_ir::IRCode,
inlinee::Method,
inlined_at::Int32)
coverage = coverage_enabled(inlinee.module)
linetable_offset::Int32 = length(linetable)
# Append the linetable of the inlined function to our line table
inlined_at = compact.result[idx][:line]
topline::Int32 = linetable_offset + Int32(1)
coverage = coverage_enabled(def.module)
coverage_by_path = JLOptions().code_coverage == 3
push!(linetable, LineInfoNode(def.module, def.name, def.file, def.line, inlined_at))
oldlinetable = spec.ir.linetable
push!(linetable, LineInfoNode(inlinee.module, inlinee.name, inlinee.file, inlinee.line, inlined_at))
oldlinetable = inlinee_ir.linetable
extra_coverage_line = 0
for oldline in 1:length(oldlinetable)
entry = oldlinetable[oldline]
if !coverage && coverage_by_path && is_file_tracked(entry.file)
Expand All @@ -341,8 +337,25 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector
end
push!(linetable, newentry)
end
if coverage && spec.ir.stmts[1][:line] + linetable_offset != topline
insert_node_here!(compact, NewInstruction(Expr(:code_coverage_effect), Nothing, topline))
if coverage && inlinee_ir.stmts[1][:line] + linetable_offset != topline
extra_coverage_line = topline
end
return linetable_offset, extra_coverage_line
end

function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any},
linetable::Vector{LineInfoNode}, item::InliningTodo,
boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}})
# Ok, do the inlining here
spec = item.spec::ResolvedInliningSpec
sparam_vals = item.mi.sparam_vals
def = item.mi.def::Method
inlined_at = compact.result[idx][:line]
linetable_offset::Int32 = length(linetable)
topline::Int32 = linetable_offset + Int32(1)
linetable_offset, extra_coverage_line = ir_inline_linetable!(linetable, item.spec.ir, def, inlined_at)
if extra_coverage_line != 0
insert_node_here!(compact, NewInstruction(Expr(:code_coverage_effect), Nothing, extra_coverage_line))
end
if def.isva
nargs_def = Int(def.nargs::Int32)
Expand Down Expand Up @@ -839,7 +852,7 @@ function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8)
src === nothing && return compileable_specialization(et, match, effects)

et !== nothing && push!(et, mi)
return InliningTodo(mi, src, effects)
return InliningTodo(mi, retrieve_ir_for_inlining(mi, src), effects)
end

function resolve_todo((; fully_covered, atype, cases, #=bbs=#)::UnionSplit, state::InliningState, flag::UInt8)
Expand All @@ -861,7 +874,8 @@ function validate_sparams(sparams::SimpleVector)
end

function analyze_method!(match::MethodMatch, argtypes::Vector{Any},
flag::UInt8, state::InliningState)
flag::UInt8, state::InliningState,
do_resolve::Bool = true)
method = match.method
spec_types = match.spec_types

Expand Down Expand Up @@ -895,26 +909,20 @@ function analyze_method!(match::MethodMatch, argtypes::Vector{Any},
todo = InliningTodo(mi, match, argtypes)
# If we don't have caches here, delay resolving this MethodInstance
# until the batch inlining step (or an external post-processing pass)
state.mi_cache === nothing && return todo
do_resolve && state.mi_cache === nothing && return todo
return resolve_todo(todo, state, flag)
end

function InliningTodo(mi::MethodInstance, ir::IRCode, effects::Effects)
ir = copy(ir)
return InliningTodo(mi, ResolvedInliningSpec(ir, effects))
end

function InliningTodo(mi::MethodInstance, src::Union{CodeInfo, Vector{UInt8}}, effects::Effects)
if !isa(src, CodeInfo)
src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo
else
src = copy(src)
end
@timeit "inline IR inflation" begin
ir = inflate_ir!(src, mi)::IRCode
return InliningTodo(mi, ResolvedInliningSpec(ir, effects))
end
function retrieve_ir_for_inlining(mi::MethodInstance, src::Array{UInt8, 1})
src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo
return inflate_ir!(src, mi)
end
retrieve_ir_for_inlining(mi::MethodInstance, src::CodeInfo) = inflate_ir(src, mi)::IRCode
retrieve_ir_for_inlining(mi::MethodInstance, ir::IRCode) = copy(ir)

function handle_single_case!(
ir::IRCode, idx::Int, stmt::Expr,
Expand Down Expand Up @@ -1196,7 +1204,7 @@ function process_simple!(ir::IRCode, idx::Int, state::InliningState, todo::Vecto
end
end

if sig.f !== Core.invoke && is_builtin(sig)
if sig.f !== Core.invoke && sig.f !== Core.finalizer && is_builtin(sig)
# No inlining for builtins (other invoke/apply/typeassert)
return nothing
end
Expand All @@ -1213,9 +1221,10 @@ function process_simple!(ir::IRCode, idx::Int, state::InliningState, todo::Vecto
end

# TODO inline non-`isdispatchtuple`, union-split callsites?
function analyze_single_call!(
ir::IRCode, idx::Int, stmt::Expr, infos::Vector{MethodMatchInfo}, flag::UInt8,
sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}})
function compute_inlining_cases(
infos::Vector{MethodMatchInfo}, flag::UInt8,
sig::Signature, state::InliningState,
do_resolve::Bool = true)
argtypes = sig.argtypes
cases = InliningCase[]
local any_fully_covered = false
Expand All @@ -1232,7 +1241,7 @@ function analyze_single_call!(
continue
end
for match in meth
handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true)
handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true, do_resolve)
any_fully_covered |= match.fully_covers
end
end
Expand All @@ -1242,8 +1251,18 @@ function analyze_single_call!(
filter!(case::InliningCase->isdispatchtuple(case.sig), cases)
end

handle_cases!(ir, idx, stmt, argtypes_to_type(argtypes), cases,
handled_all_cases & any_fully_covered, todo, state.params)
return cases, handled_all_cases & any_fully_covered
end

function analyze_single_call!(
ir::IRCode, idx::Int, stmt::Expr, infos::Vector{MethodMatchInfo}, flag::UInt8,
sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}})

r = compute_inlining_cases(infos, flag, sig, state)
r === nothing && return nothing
cases, all_covered = r
handle_cases!(ir, idx, stmt, argtypes_to_type(sig.argtypes), cases,
all_covered, todo, state.params)
end

# similar to `analyze_single_call!`, but with constant results
Expand Down Expand Up @@ -1295,14 +1314,15 @@ end

function handle_match!(
match::MethodMatch, argtypes::Vector{Any}, flag::UInt8, state::InliningState,
cases::Vector{InliningCase}, allow_abstract::Bool = false)
cases::Vector{InliningCase}, allow_abstract::Bool = false,
do_resolve::Bool = true)
spec_types = match.spec_types
allow_abstract || isdispatchtuple(spec_types) || return false
# we may see duplicated dispatch signatures here when a signature gets widened
# during abstract interpretation: for the purpose of inlining, we can just skip
# processing this dispatch candidate
_any(case->case.sig === spec_types, cases) && return true
item = analyze_method!(match, argtypes, flag, state)
item = analyze_method!(match, argtypes, flag, state, do_resolve)
item === nothing && return false
push!(cases, InliningCase(spec_types, item))
return true
Expand Down Expand Up @@ -1417,6 +1437,48 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState)
continue
end

# Handle finalizer
if sig.f === Core.finalizer
if isa(info, FinalizerInfo)
# Only inline finalizers that are known nothrow and notls.
# This avoids having to set up state for finalizer isolation
(is_nothrow(info.effects) && is_notaskstate(info.effects)) || continue

info = info.info
if isa(info, MethodMatchInfo)
infos = MethodMatchInfo[info]
elseif isa(info, UnionSplitInfo)
infos = info.matches
else
continue
end

ft = argextype(stmt.args[2], ir)
has_free_typevars(ft) && return nothing
f = singleton_type(ft)
argtypes = Vector{Any}(undef, 2)
argtypes[1] = ft
argtypes[2] = argextype(stmt.args[3], ir)
sig = Signature(f, ft, argtypes)

cases, all_covered = compute_inlining_cases(infos, UInt8(0), sig, state, false)
length(cases) == 0 && continue
if all_covered && length(cases) == 1
if isa(cases[1], InliningCase)
case1 = cases[1].item
if isa(case1, InliningTodo)
push!(stmt.args, true)
push!(stmt.args, case1.mi)
elseif isa(case1, InvokeCase)
push!(stmt.args, false)
push!(stmt.args, case1.invoke)
end
end
end
continue
end
end

# if inference arrived here with constant-prop'ed result(s),
# we can perform a specialized analysis for just this case
if isa(info, ConstCallInfo)
Expand Down
60 changes: 30 additions & 30 deletions base/compiler/ssair/ir.jl
Original file line number Diff line number Diff line change
Expand Up @@ -163,36 +163,6 @@ const AnySSAValue = Union{SSAValue, OldSSAValue, NewSSAValue}


# SSA-indexed nodes

struct NewInstruction
stmt::Any
type::Any
info::Any
# If nothing, copy the line from previous statement
# in the insertion location
line::Union{Int32, Nothing}
flag::UInt8

## Insertion options

# The IR_FLAG_EFFECT_FREE flag has already been computed (or forced).
# Don't bother redoing so on insertion.
effect_free_computed::Bool
NewInstruction(@nospecialize(stmt), @nospecialize(type), @nospecialize(info),
line::Union{Int32, Nothing}, flag::UInt8, effect_free_computed::Bool) =
new(stmt, type, info, line, flag, effect_free_computed)
end
NewInstruction(@nospecialize(stmt), @nospecialize(type)) =
NewInstruction(stmt, type, nothing)
NewInstruction(@nospecialize(stmt), @nospecialize(type), line::Union{Nothing, Int32}) =
NewInstruction(stmt, type, nothing, line, IR_FLAG_NULL, false)

effect_free(inst::NewInstruction) =
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag | IR_FLAG_EFFECT_FREE, true)
non_effect_free(inst::NewInstruction) =
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag & ~IR_FLAG_EFFECT_FREE, true)


struct InstructionStream
inst::Vector{Any}
type::Vector{Any}
Expand Down Expand Up @@ -292,6 +262,36 @@ function add!(new::NewNodeStream, pos::Int, attach_after::Bool)
end
copy(nns::NewNodeStream) = NewNodeStream(copy(nns.stmts), copy(nns.info))

struct NewInstruction
stmt::Any
type::Any
info::Any
# If nothing, copy the line from previous statement
# in the insertion location
line::Union{Int32, Nothing}
flag::UInt8

## Insertion options

# The IR_FLAG_EFFECT_FREE flag has already been computed (or forced).
# Don't bother redoing so on insertion.
effect_free_computed::Bool
NewInstruction(@nospecialize(stmt), @nospecialize(type), @nospecialize(info),
line::Union{Int32, Nothing}, flag::UInt8, effect_free_computed::Bool) =
new(stmt, type, info, line, flag, effect_free_computed)
end
NewInstruction(@nospecialize(stmt), @nospecialize(type)) =
NewInstruction(stmt, type, nothing)
NewInstruction(@nospecialize(stmt), @nospecialize(type), line::Union{Nothing, Int32}) =
NewInstruction(stmt, type, nothing, line, IR_FLAG_NULL, false)
NewInstruction(@nospecialize(stmt), meta::Instruction; line::Union{Int32, Nothing}=nothing) =
NewInstruction(stmt, meta[:type], meta[:info], line === nothing ? meta[:line] : line, meta[:flag], true)

effect_free(inst::NewInstruction) =
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag | IR_FLAG_EFFECT_FREE, true)
non_effect_free(inst::NewInstruction) =
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag & ~IR_FLAG_EFFECT_FREE, true)

struct IRCode
stmts::InstructionStream
argtypes::Vector{Any}
Expand Down
Loading

2 comments on commit c4effda

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily package evaluation, I will reply here when finished:

@nanosoldier runtests(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your package evaluation job has completed - possible new issues were detected. A full report can be found here.

Please sign in to comment.