Skip to content

Commit 7c49f16

Browse files
committed
inlining: use already-resolved source for finalizer inlining
The source for `Core.finalizer` call is already resolved when arrived at `sroa_mutables!`, so we can just try to inline `InliningCase` appended to the intrinsic call there. This commit also improves the robustness of the implementation, by handling few edge cases e.g. when `finalizer` returns a constant value and when the `finalizer` inliner bail out from inlining `finalizer` with conditional block, and tests such cases explicitly.
1 parent f563c26 commit 7c49f16

File tree

4 files changed

+78
-58
lines changed

4 files changed

+78
-58
lines changed

base/compiler/optimize.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ function run_passes(
578578
@pass "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds)
579579
# @timeit "verify 2" verify_ir(ir)
580580
@pass "compact 2" ir = compact!(ir)
581-
@pass "SROA" ir = sroa_pass!(ir, sv.inlining)
581+
@pass "SROA" ir = sroa_pass!(ir)
582582
@pass "ADCE" ir = adce_pass!(ir)
583583
@pass "type lift" ir = type_lift_pass!(ir)
584584
@pass "compact 3" ir = compact!(ir)

base/compiler/ssair/inlining.jl

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,10 @@ function handle_finalizer_call!(
13971397
infos = MethodMatchInfo[info]
13981398
elseif isa(info, UnionSplitInfo)
13991399
infos = info.matches
1400+
# elseif isa(info, ConstCallInfo)
1401+
# # NOTE currently this code path isn't active as constant propagation won't happen
1402+
# # for `Core.finalizer` call because inference currently isn't able to fold a mutable
1403+
# # object as a constant
14001404
else
14011405
return nothing
14021406
end
@@ -1413,14 +1417,7 @@ function handle_finalizer_call!(
14131417
cases === nothing && return nothing
14141418
cases, all_covered = cases
14151419
if all_covered && length(cases) == 1
1416-
item1 = cases[1].item
1417-
if isa(item1, InliningTodo)
1418-
push!(stmt.args, true)
1419-
push!(stmt.args, item1.mi)
1420-
elseif isa(item1, InvokeCase)
1421-
push!(stmt.args, false)
1422-
push!(stmt.args, item1.invoke)
1423-
end
1420+
push!(stmt.args, cases[1].item)
14241421
end
14251422
return nothing
14261423
end

base/compiler/ssair/passes.jl

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ its argument).
742742
In a case when all usages are fully eliminated, `struct` allocation may also be erased as
743743
a result of succeeding dead code elimination.
744744
"""
745-
function sroa_pass!(ir::IRCode, inlining::Union{Nothing, InliningState} = nothing)
745+
function sroa_pass!(ir::IRCode)
746746
compact = IncrementalCompact(ir)
747747
defuses = nothing # will be initialized once we encounter mutability in order to reduce dynamic allocations
748748
lifting_cache = IdDict{Pair{AnySSAValue, Any}, AnySSAValue}()
@@ -775,11 +775,11 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing, InliningState} = nothin
775775
widenconst(field_ordering) === Bool && (field_ordering = :unspecified)
776776
end
777777
elseif is_known_call(stmt, Core.finalizer, compact)
778-
3 <= length(stmt.args) <= 5 || continue
778+
3 <= length(stmt.args) <= 4 || continue
779779
# Inlining performs legality checks on the finalizer to determine
780780
# whether or not we may inline it. If so, it appends extra arguments
781781
# at the end of the intrinsic. Detect that here.
782-
length(stmt.args) == 5 || continue
782+
length(stmt.args) == 4 || continue
783783
is_finalizer = true
784784
elseif isexpr(stmt, :foreigncall)
785785
nccallargs = length(stmt.args[3]::SimpleVector)
@@ -940,44 +940,40 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing, InliningState} = nothin
940940
used_ssas = copy(compact.used_ssas)
941941
simple_dce!(compact, (x::SSAValue) -> used_ssas[x.id] -= 1)
942942
ir = complete(compact)
943-
sroa_mutables!(ir, defuses, used_ssas, lazydomtree, inlining)
943+
sroa_mutables!(ir, defuses, used_ssas, lazydomtree)
944944
return ir
945945
else
946946
simple_dce!(compact)
947947
return complete(compact)
948948
end
949949
end
950950

951-
function try_inline_finalizer!(ir::IRCode, argexprs::Vector{Any}, idx::Int, mi::MethodInstance, inlining::InliningState)
952-
code = get(inlining.mi_cache, mi, nothing)
953-
if code isa CodeInstance
954-
if use_const_api(code)
955-
# No code in the function - Nothing to do
956-
inlining.et !== nothing && push!(inlining.et, mi)
957-
return true
958-
end
959-
src = code.inferred
960-
else
961-
src = code
951+
function inline_finalizer!(ir::IRCode, argexprs::Vector{Any}, idx::Int, @nospecialize(case))
952+
if isa(case, ConstantCase)
953+
# No code in the function - Nothing to do
954+
return true
955+
elseif isa(case, InvokeCase)
956+
insert_node!(ir, idx, NewInstruction(Expr(:invoke, case.invoke, argexprs...), Nothing), true)
957+
return true
958+
elseif case === nothing
959+
@assert false "this case should never happen"
962960
end
963961

964-
src = inlining_policy(inlining.interp, src, IR_FLAG_NULL, mi, Any[])
965-
src === nothing && return false
966-
src = retrieve_ir_for_inlining(mi, src)
962+
# now try to inline the finalizer body
963+
(; mi, spec) = case::InliningTodo
964+
src = (spec::ResolvedInliningSpec).ir
967965

968966
# For now: Require finalizer to only have one basic block
969967
length(src.cfg.blocks) == 1 || return false
970968

971969
# Ok, we're committed to inlining the finalizer
972-
inlining.et !== nothing && push!(inlining.et, mi)
973970

974971
linetable_offset, extra_coverage_line = ir_inline_linetable!(ir.linetable, src, mi.def, ir[SSAValue(idx)][:line])
975972
if extra_coverage_line != 0
976973
insert_node!(ir, idx, NewInstruction(Expr(:code_coverage_effect), Nothing, extra_coverage_line))
977974
end
978975

979-
# TODO: Use the actual inliner here rather than open coding this special
980-
# purpose inliner.
976+
# TODO: Use the actual inliner here rather than open coding this special purpose inliner.
981977
spvals = mi.sparam_vals
982978
ssa_rename = Vector{Any}(undef, length(src.stmts))
983979
for idx′ = 1:length(src.stmts)
@@ -1002,7 +998,7 @@ end
1002998

1003999
is_nothrow(ir::IRCode, pc::Int) = ir.stmts[pc][:flag] & (IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW) 0
10041000

1005-
function try_inline_finalizer!(ir::IRCode, idx::Int, finalizer_idx::Int, defuse::SSADefUse, inlining::InliningState)
1001+
function try_inline_finalizer!(ir::IRCode, idx::Int, finalizer_idx::Int, defuse::SSADefUse)
10061002
# For now: Require that all uses and defs are in the same basic block,
10071003
# so that live range calculations are easy.
10081004
bb = ir.cfg.blocks[block_for_inst(ir.cfg, first(defuse.uses).idx)]
@@ -1025,40 +1021,30 @@ function try_inline_finalizer!(ir::IRCode, idx::Int, finalizer_idx::Int, defuse:
10251021
return true
10261022
end
10271023

1028-
check_in_range(idx) || return
1029-
_all(check_in_range, defuse.uses) || return
1030-
_all(check_in_range, defuse.defs) || return
1024+
check_in_range(idx) || return nothing
1025+
_all(check_in_range, defuse.uses) || return nothing
1026+
_all(check_in_range, defuse.defs) || return nothing
10311027

10321028
# For now: Require all statements in the basic block range to be nothrow.
10331029
_all(minval:maxval) do idx::Int
10341030
return is_nothrow(ir, idx) || idx == finalizer_idx
1035-
end || return
1031+
end || return nothing
10361032

10371033
# Ok, finalizer rewrite is legal.
10381034
finalizer_stmt = ir[SSAValue(finalizer_idx)][:inst]
10391035
argexprs = Any[finalizer_stmt.args[2], finalizer_stmt.args[3]]
1040-
mi = finalizer_stmt.args[5]::MethodInstance
1041-
if finalizer_stmt.args[4]::Bool # may inline
1042-
if try_inline_finalizer!(ir, argexprs, maxval, mi, inlining)
1043-
@goto done_finalizer
1044-
end
1045-
mi = compileable_specialization(inlining.et, mi, Effects()).invoke
1046-
end
1047-
if mi !== nothing
1048-
insert_node!(ir, maxval,
1049-
NewInstruction(Expr(:invoke, mi, argexprs...), Nothing),
1050-
true)
1036+
case = finalizer_stmt.args[4]
1037+
if inline_finalizer!(ir, argexprs, maxval, case)
1038+
# Erase call to finalizer
1039+
ir[SSAValue(finalizer_idx)][:inst] = nothing
10511040
else
1052-
insert_node!(ir, maxval,
1053-
NewInstruction(Expr(:call, argexprs...), Nothing),
1054-
true)
1041+
pop!(finalizer_stmt.args)
1042+
@assert length(finalizer_stmt.args) == 3
10551043
end
1056-
@label done_finalizer
1057-
# Erase call to finalizer
1058-
ir[SSAValue(finalizer_idx)][:inst] = nothing
1044+
return nothing
10591045
end
10601046

1061-
function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse}}, used_ssas::Vector{Int}, lazydomtree::LazyDomtree, inlining::Union{Nothing, InliningState})
1047+
function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse}}, used_ssas::Vector{Int}, lazydomtree::LazyDomtree)
10621048
for (idx, (intermediaries, defuse)) in defuses
10631049
intermediaries = collect(intermediaries)
10641050
# Check if there are any uses we did not account for. If so, the variable
@@ -1090,8 +1076,8 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse
10901076
finalizer_idx = use.idx
10911077
end
10921078
end
1093-
if finalizer_idx !== nothing && inlining !== nothing
1094-
try_inline_finalizer!(ir, idx, finalizer_idx, defuse, inlining)
1079+
if finalizer_idx !== nothing
1080+
try_inline_finalizer!(ir, idx, finalizer_idx, defuse)
10951081
continue
10961082
end
10971083
# Partition defuses by field

test/compiler/inline.jl

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,7 +1300,6 @@ mutable struct DoAllocNoEscape
13001300
end
13011301
end
13021302
end
1303-
13041303
let src = code_typed1() do
13051304
for i = 1:1000
13061305
DoAllocNoEscape()
@@ -1309,6 +1308,22 @@ let src = code_typed1() do
13091308
@test count(isnew, src.code) == 0
13101309
end
13111310

1311+
# Test that we can inline a finalizer that just returns a constant value
1312+
mutable struct DoAllocConst
1313+
function DoAllocConst()
1314+
finalizer(new()) do this
1315+
return nothing
1316+
end
1317+
end
1318+
end
1319+
let src = code_typed1() do
1320+
for i = 1:1000
1321+
DoAllocConst()
1322+
end
1323+
end
1324+
@test count(isnew, src.code) == 0
1325+
end
1326+
13121327
# Test that finalizer elision doesn't cause a throw to be inlined into a function
13131328
# that shouldn't have it
13141329
const finalizer_should_throw = Ref{Bool}(true)
@@ -1345,7 +1360,6 @@ mutable struct DoAllocNoEscapeSparam{T}
13451360
end
13461361
end
13471362
DoAllocNoEscapeSparam(x::T) where {T} = DoAllocNoEscapeSparam{T}(x)
1348-
13491363
let src = code_typed1(Tuple{Any}) do x
13501364
for i = 1:1000
13511365
DoAllocNoEscapeSparam(x)
@@ -1365,7 +1379,6 @@ mutable struct DoAllocNoEscapeNoInline
13651379
finalizer(noinline_finalizer, new())
13661380
end
13671381
end
1368-
13691382
let src = code_typed1() do
13701383
for i = 1:1000
13711384
DoAllocNoEscapeNoInline()
@@ -1375,6 +1388,30 @@ let src = code_typed1() do
13751388
@test count(isinvoke(:noinline_finalizer), src.code) == 1
13761389
end
13771390

1391+
# Test that we don't form invalid `finalizer` call for cases we don't handle currently
1392+
mutable struct DoAllocNoEscapeBranch
1393+
val::Int
1394+
function DoAllocNoEscapeBranch(val::Int)
1395+
finalizer(new(val)) do this
1396+
if this.val > 500
1397+
nothrow_side_effect(this.val)
1398+
else
1399+
nothrow_side_effect(nothing)
1400+
end
1401+
end
1402+
end
1403+
end
1404+
let src = code_typed1() do
1405+
for i = 1:1000
1406+
DoAllocNoEscapeBranch(i)
1407+
end
1408+
end
1409+
@test any(src.code) do @nospecialize x
1410+
iscall((src, Core.finalizer))(x) &&
1411+
length(x.args) == 3
1412+
end
1413+
end
1414+
13781415
# optimize `[push!|pushfirst!](::Vector{Any}, x...)`
13791416
@testset "optimize `$f(::Vector{Any}, x...)`" for f = Any[push!, pushfirst!]
13801417
@eval begin

0 commit comments

Comments
 (0)