Skip to content

Commit 8f4ed29

Browse files
committed
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 } ```
1 parent ba4a4b2 commit 8f4ed29

File tree

9 files changed

+381
-78
lines changed

9 files changed

+381
-78
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,15 @@ function invoke_rewrite(xs::Vector{Any})
15891589
return newxs
15901590
end
15911591

1592+
function abstract_finalizer(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState)
1593+
if length(argtypes) == 3
1594+
finalizer_argvec = Any[argtypes[2], argtypes[3]]
1595+
call = abstract_call(interp, ArgInfo(nothing, finalizer_argvec), sv, 1)
1596+
return CallMeta(Nothing, Effects(), FinalizerInfo(call.info, call.effects))
1597+
end
1598+
return CallMeta(Nothing, Effects(), false)
1599+
end
1600+
15921601
# call where the function is known exactly
15931602
function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
15941603
arginfo::ArgInfo, sv::InferenceState,
@@ -1603,6 +1612,8 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
16031612
return abstract_invoke(interp, arginfo, sv)
16041613
elseif f === modifyfield!
16051614
return abstract_modifyfield!(interp, argtypes, sv)
1615+
elseif f === Core.finalizer
1616+
return abstract_finalizer(interp, argtypes, sv)
16061617
end
16071618
rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods)
16081619
return CallMeta(rt, builtin_effects(f, argtypes, rt), false)

base/compiler/optimize.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const IR_FLAG_THROW_BLOCK = 0x01 << 3
2727
# This statement may be removed if its result is unused. In particular it must
2828
# thus be both pure and effect free.
2929
const IR_FLAG_EFFECT_FREE = 0x01 << 4
30+
# This statement was proven not to throw
31+
const IR_FLAG_NOTHROW = 0x01 << 5
32+
3033

3134
const TOP_TUPLE = GlobalRef(Core, :tuple)
3235

@@ -564,7 +567,7 @@ function run_passes(
564567
@pass "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds)
565568
# @timeit "verify 2" verify_ir(ir)
566569
@pass "compact 2" ir = compact!(ir)
567-
@pass "SROA" ir = sroa_pass!(ir)
570+
@pass "SROA" ir = sroa_pass!(ir, sv.inlining)
568571
@pass "ADCE" ir = adce_pass!(ir)
569572
@pass "type lift" ir = type_lift_pass!(ir)
570573
@pass "compact 3" ir = compact!(ir)

base/compiler/ssair/inlining.jl

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -308,21 +308,17 @@ function finish_cfg_inline!(state::CFGInliningState)
308308
end
309309
end
310310

311-
function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any},
312-
linetable::Vector{LineInfoNode}, item::InliningTodo,
313-
boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}})
314-
# Ok, do the inlining here
315-
spec = item.spec::ResolvedInliningSpec
316-
sparam_vals = item.mi.sparam_vals
317-
def = item.mi.def::Method
311+
function ir_inline_linetable!(linetable::Vector{LineInfoNode}, inlinee_ir::IRCode,
312+
inlinee::Method,
313+
inlined_at::Int32)
314+
coverage = coverage_enabled(inlinee.module)
318315
linetable_offset::Int32 = length(linetable)
319316
# Append the linetable of the inlined function to our line table
320-
inlined_at = compact.result[idx][:line]
321317
topline::Int32 = linetable_offset + Int32(1)
322-
coverage = coverage_enabled(def.module)
323318
coverage_by_path = JLOptions().code_coverage == 3
324-
push!(linetable, LineInfoNode(def.module, def.name, def.file, def.line, inlined_at))
325-
oldlinetable = spec.ir.linetable
319+
push!(linetable, LineInfoNode(inlinee.module, inlinee.name, inlinee.file, inlinee.line, inlined_at))
320+
oldlinetable = inlinee_ir.linetable
321+
extra_coverage_line = 0
326322
for oldline in 1:length(oldlinetable)
327323
entry = oldlinetable[oldline]
328324
if !coverage && coverage_by_path && is_file_tracked(entry.file)
@@ -341,8 +337,25 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector
341337
end
342338
push!(linetable, newentry)
343339
end
344-
if coverage && spec.ir.stmts[1][:line] + linetable_offset != topline
345-
insert_node_here!(compact, NewInstruction(Expr(:code_coverage_effect), Nothing, topline))
340+
if coverage && inlinee_ir.stmts[1][:line] + linetable_offset != topline
341+
extra_coverage_line = topline
342+
end
343+
return linetable_offset, extra_coverage_line
344+
end
345+
346+
function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any},
347+
linetable::Vector{LineInfoNode}, item::InliningTodo,
348+
boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}})
349+
# Ok, do the inlining here
350+
spec = item.spec::ResolvedInliningSpec
351+
sparam_vals = item.mi.sparam_vals
352+
def = item.mi.def::Method
353+
inlined_at = compact.result[idx][:line]
354+
linetable_offset::Int32 = length(linetable)
355+
topline::Int32 = linetable_offset + Int32(1)
356+
linetable_offset, extra_coverage_line = ir_inline_linetable!(linetable, item.spec.ir, def, inlined_at)
357+
if extra_coverage_line != 0
358+
insert_node_here!(compact, NewInstruction(Expr(:code_coverage_effect), Nothing, extra_coverage_line))
346359
end
347360
if def.isva
348361
nargs_def = Int(def.nargs::Int32)
@@ -848,7 +861,7 @@ function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8)
848861
src === nothing && return compileable_specialization(et, match, effects)
849862

850863
et !== nothing && push!(et, mi)
851-
return InliningTodo(mi, src, effects)
864+
return InliningTodo(mi, retrieve_ir_for_inlining(mi, src), effects)
852865
end
853866

854867
function resolve_todo((; fully_covered, atype, cases, #=bbs=#)::UnionSplit, state::InliningState, flag::UInt8)
@@ -870,7 +883,8 @@ function validate_sparams(sparams::SimpleVector)
870883
end
871884

872885
function analyze_method!(match::MethodMatch, argtypes::Vector{Any},
873-
flag::UInt8, state::InliningState)
886+
flag::UInt8, state::InliningState,
887+
do_resolve::Bool = true)
874888
method = match.method
875889
spec_types = match.spec_types
876890

@@ -904,7 +918,7 @@ function analyze_method!(match::MethodMatch, argtypes::Vector{Any},
904918
todo = InliningTodo(mi, match, argtypes)
905919
# If we don't have caches here, delay resolving this MethodInstance
906920
# until the batch inlining step (or an external post-processing pass)
907-
state.mi_cache === nothing && return todo
921+
do_resolve && state.mi_cache === nothing && return todo
908922
return resolve_todo(todo, state, flag)
909923
end
910924

@@ -913,17 +927,12 @@ function InliningTodo(mi::MethodInstance, ir::IRCode, effects::Effects)
913927
return InliningTodo(mi, ResolvedInliningSpec(ir, effects))
914928
end
915929

916-
function InliningTodo(mi::MethodInstance, src::Union{CodeInfo, Vector{UInt8}}, effects::Effects)
917-
if !isa(src, CodeInfo)
918-
src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo
919-
else
920-
src = copy(src)
921-
end
922-
@timeit "inline IR inflation" begin
923-
ir = inflate_ir!(src, mi)::IRCode
924-
return InliningTodo(mi, ResolvedInliningSpec(ir, effects))
925-
end
930+
function retrieve_ir_for_inlining(mi::MethodInstance, src::Array{UInt8, 1})
931+
src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo
932+
return inflate_ir!(src, mi)
926933
end
934+
retrieve_ir_for_inlining(mi::MethodInstance, src::CodeInfo) = inflate_ir(src, mi)::IRCode
935+
retrieve_ir_for_inlining(mi::MethodInstance, ir::IRCode) = copy(ir)
927936

928937
function handle_single_case!(
929938
ir::IRCode, idx::Int, stmt::Expr,
@@ -1205,7 +1214,7 @@ function process_simple!(ir::IRCode, idx::Int, state::InliningState, todo::Vecto
12051214
end
12061215
end
12071216

1208-
if sig.f !== Core.invoke && is_builtin(sig)
1217+
if sig.f !== Core.invoke && sig.f !== Core.finalizer && is_builtin(sig)
12091218
# No inlining for builtins (other invoke/apply/typeassert)
12101219
return nothing
12111220
end
@@ -1222,9 +1231,10 @@ function process_simple!(ir::IRCode, idx::Int, state::InliningState, todo::Vecto
12221231
end
12231232

12241233
# TODO inline non-`isdispatchtuple`, union-split callsites?
1225-
function analyze_single_call!(
1226-
ir::IRCode, idx::Int, stmt::Expr, infos::Vector{MethodMatchInfo}, flag::UInt8,
1227-
sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}})
1234+
function compute_inlining_cases(
1235+
infos::Vector{MethodMatchInfo}, flag::UInt8,
1236+
sig::Signature, state::InliningState,
1237+
do_resolve::Bool = true)
12281238
argtypes = sig.argtypes
12291239
cases = InliningCase[]
12301240
local any_fully_covered = false
@@ -1241,7 +1251,7 @@ function analyze_single_call!(
12411251
continue
12421252
end
12431253
for match in meth
1244-
handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true)
1254+
handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true, do_resolve)
12451255
any_fully_covered |= match.fully_covers
12461256
end
12471257
end
@@ -1251,8 +1261,18 @@ function analyze_single_call!(
12511261
filter!(case::InliningCase->isdispatchtuple(case.sig), cases)
12521262
end
12531263

1254-
handle_cases!(ir, idx, stmt, argtypes_to_type(argtypes), cases,
1255-
handled_all_cases & any_fully_covered, todo, state.params)
1264+
return cases, handled_all_cases & any_fully_covered
1265+
end
1266+
1267+
function analyze_single_call!(
1268+
ir::IRCode, idx::Int, stmt::Expr, infos::Vector{MethodMatchInfo}, flag::UInt8,
1269+
sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}})
1270+
1271+
r = compute_inlining_cases(infos, flag, sig, state)
1272+
r === nothing && return nothing
1273+
cases, all_covered = r
1274+
handle_cases!(ir, idx, stmt, argtypes_to_type(sig.argtypes), cases,
1275+
all_covered, todo, state.params)
12561276
end
12571277

12581278
# similar to `analyze_single_call!`, but with constant results
@@ -1304,14 +1324,15 @@ end
13041324

13051325
function handle_match!(
13061326
match::MethodMatch, argtypes::Vector{Any}, flag::UInt8, state::InliningState,
1307-
cases::Vector{InliningCase}, allow_abstract::Bool = false)
1327+
cases::Vector{InliningCase}, allow_abstract::Bool = false,
1328+
do_resolve::Bool = true)
13081329
spec_types = match.spec_types
13091330
allow_abstract || isdispatchtuple(spec_types) || return false
13101331
# we may see duplicated dispatch signatures here when a signature gets widened
13111332
# during abstract interpretation: for the purpose of inlining, we can just skip
13121333
# processing this dispatch candidate
13131334
_any(case->case.sig === spec_types, cases) && return true
1314-
item = analyze_method!(match, argtypes, flag, state)
1335+
item = analyze_method!(match, argtypes, flag, state, do_resolve)
13151336
item === nothing && return false
13161337
push!(cases, InliningCase(spec_types, item))
13171338
return true
@@ -1424,6 +1445,48 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState)
14241445
continue
14251446
end
14261447

1448+
# Handle finalizer
1449+
if sig.f === Core.finalizer
1450+
if isa(info, FinalizerInfo)
1451+
# Only inline finalizers that are known nothrow and notls.
1452+
# This avoids having to set up state for finalizer isolation
1453+
(is_nothrow(info.effects) && is_notaskstate(info.effects)) || continue
1454+
1455+
info = info.info
1456+
if isa(info, MethodMatchInfo)
1457+
infos = MethodMatchInfo[info]
1458+
elseif isa(info, UnionSplitInfo)
1459+
infos = info.matches
1460+
else
1461+
continue
1462+
end
1463+
1464+
ft = argextype(stmt.args[2], ir)
1465+
has_free_typevars(ft) && return nothing
1466+
f = singleton_type(ft)
1467+
argtypes = Vector{Any}(undef, 2)
1468+
argtypes[1] = ft
1469+
argtypes[2] = argextype(stmt.args[3], ir)
1470+
sig = Signature(f, ft, argtypes)
1471+
1472+
cases, all_covered = compute_inlining_cases(infos, UInt8(0), sig, state, false)
1473+
length(cases) == 0 && continue
1474+
if all_covered && length(cases) == 1
1475+
if isa(cases[1], InliningCase)
1476+
case1 = cases[1].item
1477+
if isa(case1, InliningTodo)
1478+
push!(stmt.args, true)
1479+
push!(stmt.args, case1.mi)
1480+
elseif isa(case1, InvokeCase)
1481+
push!(stmt.args, false)
1482+
push!(stmt.args, case1.invoke)
1483+
end
1484+
end
1485+
end
1486+
continue
1487+
end
1488+
end
1489+
14271490
# if inference arrived here with constant-prop'ed result(s),
14281491
# we can perform a specialized analysis for just this case
14291492
if isa(info, ConstCallInfo)

base/compiler/ssair/ir.jl

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -163,36 +163,6 @@ const AnySSAValue = Union{SSAValue, OldSSAValue, NewSSAValue}
163163

164164

165165
# SSA-indexed nodes
166-
167-
struct NewInstruction
168-
stmt::Any
169-
type::Any
170-
info::Any
171-
# If nothing, copy the line from previous statement
172-
# in the insertion location
173-
line::Union{Int32, Nothing}
174-
flag::UInt8
175-
176-
## Insertion options
177-
178-
# The IR_FLAG_EFFECT_FREE flag has already been computed (or forced).
179-
# Don't bother redoing so on insertion.
180-
effect_free_computed::Bool
181-
NewInstruction(@nospecialize(stmt), @nospecialize(type), @nospecialize(info),
182-
line::Union{Int32, Nothing}, flag::UInt8, effect_free_computed::Bool) =
183-
new(stmt, type, info, line, flag, effect_free_computed)
184-
end
185-
NewInstruction(@nospecialize(stmt), @nospecialize(type)) =
186-
NewInstruction(stmt, type, nothing)
187-
NewInstruction(@nospecialize(stmt), @nospecialize(type), line::Union{Nothing, Int32}) =
188-
NewInstruction(stmt, type, nothing, line, IR_FLAG_NULL, false)
189-
190-
effect_free(inst::NewInstruction) =
191-
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag | IR_FLAG_EFFECT_FREE, true)
192-
non_effect_free(inst::NewInstruction) =
193-
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag & ~IR_FLAG_EFFECT_FREE, true)
194-
195-
196166
struct InstructionStream
197167
inst::Vector{Any}
198168
type::Vector{Any}
@@ -292,6 +262,36 @@ function add!(new::NewNodeStream, pos::Int, attach_after::Bool)
292262
end
293263
copy(nns::NewNodeStream) = NewNodeStream(copy(nns.stmts), copy(nns.info))
294264

265+
struct NewInstruction
266+
stmt::Any
267+
type::Any
268+
info::Any
269+
# If nothing, copy the line from previous statement
270+
# in the insertion location
271+
line::Union{Int32, Nothing}
272+
flag::UInt8
273+
274+
## Insertion options
275+
276+
# The IR_FLAG_EFFECT_FREE flag has already been computed (or forced).
277+
# Don't bother redoing so on insertion.
278+
effect_free_computed::Bool
279+
NewInstruction(@nospecialize(stmt), @nospecialize(type), @nospecialize(info),
280+
line::Union{Int32, Nothing}, flag::UInt8, effect_free_computed::Bool) =
281+
new(stmt, type, info, line, flag, effect_free_computed)
282+
end
283+
NewInstruction(@nospecialize(stmt), @nospecialize(type)) =
284+
NewInstruction(stmt, type, nothing)
285+
NewInstruction(@nospecialize(stmt), @nospecialize(type), line::Union{Nothing, Int32}) =
286+
NewInstruction(stmt, type, nothing, line, IR_FLAG_NULL, false)
287+
NewInstruction(@nospecialize(stmt), meta::Instruction; line::Union{Int32, Nothing}=nothing) =
288+
NewInstruction(stmt, meta[:type], meta[:info], line === nothing ? meta[:line] : line, meta[:flag], true)
289+
290+
effect_free(inst::NewInstruction) =
291+
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag | IR_FLAG_EFFECT_FREE, true)
292+
non_effect_free(inst::NewInstruction) =
293+
NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag & ~IR_FLAG_EFFECT_FREE, true)
294+
295295
struct IRCode
296296
stmts::InstructionStream
297297
argtypes::Vector{Any}

0 commit comments

Comments
 (0)