Skip to content

Commit

Permalink
optimizer: early finalize insertion
Browse files Browse the repository at this point in the history
Currently, in the finalizer inlining pass, if not all the code between
the finalizer registration and the end of the object’s lifetime (i.e.,
where the finalizer would be inlined) is marked as `:nothrow`, it simply
bails out. However, even in such cases, we can insert a `finalize` call
at the end of the object’s lifetime, allowing us to call the finalizer
early if no exceptions occur.

This commit implements this optimization. To do so, it also moves
`finalize` to `Core`, so the compiler can handle it directly.
  • Loading branch information
aviatesk committed Oct 4, 2024
1 parent 7986e17 commit 522c754
Show file tree
Hide file tree
Showing 10 changed files with 51 additions and 23 deletions.
3 changes: 1 addition & 2 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,7 @@ end

BUILDROOT::String = ""

baremodule BuildSettings
end
baremodule BuildSettings end

let i = 1
global BUILDROOT
Expand Down
2 changes: 1 addition & 1 deletion base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export
nfields, throw, tuple, ===, isdefined, eval,
# access to globals
getglobal, setglobal!, swapglobal!, modifyglobal!, replaceglobal!, setglobalonce!,
# ifelse, sizeof # not exported, to avoid conflicting with Base
# ifelse, sizeof, finalize # not exported, to avoid conflicting with Base
# type reflection
<:, typeof, isa, typeassert,
# method reflection
Expand Down
34 changes: 19 additions & 15 deletions base/compiler/ssair/passes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1625,19 +1625,18 @@ function try_resolve_finalizer!(ir::IRCode, alloc_idx::Int, finalizer_idx::Int,
foreach(note_defuse!, defuse.defs)
insert_bb != 0 || return nothing # verify post-dominator of all uses exists

# Figure out the exact statement where we're going to inline the finalizer.
loc = insert_idx === nothing ? first(ir.cfg.blocks[insert_bb].stmts) : insert_idx::Int
attach_after = insert_idx !== nothing
flag = info isa FinalizerInfo ? flags_for_effects(info.effects) : IR_FLAG_NULL
finalizer_stmt = ir[SSAValue(finalizer_idx)][:stmt]

if !OptimizationParams(inlining.interp).assume_fatal_throw
# Collect all reachable blocks between the finalizer registration and the
# insertion point
blocks = reachable_blocks(ir.cfg, finalizer_bb, insert_bb)

# Check #3
function check_range_nothrow(s::Int, e::Int)
return all(s:e) do sidx::Int
sidx == finalizer_idx && return true
sidx == alloc_idx && return true
return is_nothrow(ir, SSAValue(sidx))
end
end
for bb in blocks
range = ir.cfg.blocks[bb].stmts
s, e = first(range), last(range)
Expand All @@ -1648,18 +1647,23 @@ function try_resolve_finalizer!(ir::IRCode, alloc_idx::Int, finalizer_idx::Int,
if bb == finalizer_bb
s = finalizer_idx
end
check_range_nothrow(s, e) || return nothing
all(s:e) do sidx::Int
sidx == finalizer_idx && return true
sidx == alloc_idx && return true
return is_nothrow(ir, SSAValue(sidx))
end && continue

# An exception may be thrown between the finalizer registration and the point
# where the object’s lifetime ends (`insert_idx`): In such cases, we can’t
# remove the finalizer registration, but we can still insert a `Core.finalize`
# call at `insert_idx` while leaving the registration intact.
newinst = add_flag(NewInstruction(Expr(:call, GlobalRef(Core, :finalize), finalizer_stmt.args[3]), Nothing), flag)
insert_node!(ir, loc, newinst, attach_after)
return nothing
end
end

# Ok, legality check complete. Figure out the exact statement where we're
# going to inline the finalizer.
loc = insert_idx === nothing ? first(ir.cfg.blocks[insert_bb].stmts) : insert_idx::Int
attach_after = insert_idx !== nothing

finalizer_stmt = ir[SSAValue(finalizer_idx)][:stmt]
argexprs = Any[finalizer_stmt.args[2], finalizer_stmt.args[3]]
flag = info isa FinalizerInfo ? flags_for_effects(info.effects) : IR_FLAG_NULL
if length(finalizer_stmt.args) >= 4
inline = finalizer_stmt.args[4]
if inline === nothing
Expand Down
6 changes: 5 additions & 1 deletion base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ add_tfunc(donotdelete, 0, INT_INF, @nospecs((𝕃::AbstractLattice, args...)->No
end
add_tfunc(compilerbarrier, 2, 2, compilerbarrier_tfunc, 5)
add_tfunc(Core.finalizer, 2, 4, @nospecs((𝕃::AbstractLattice, args...)->Nothing), 5)
add_tfunc(Core.finalize, 1, 1, @nospecs((𝕃::AbstractLattice, o)->Nothing), 100)

@nospecs function compilerbarrier_nothrow(setting, val)
return isa(setting, Const) && contains_is((:type, :const, :conditional), setting.val)
Expand Down Expand Up @@ -2288,8 +2289,11 @@ function _builtin_nothrow(𝕃::AbstractLattice, @nospecialize(f::Builtin), argt
return true
elseif f === Core.finalizer
2 <= na <= 4 || return false
# Core.finalizer does no error checking - that's done in Base.finalizer
# `Core.finalizer` does no error checking - that's done in Base.finalizer
return true
elseif f === Core.finalize
na == 2 || return false
return true # `Core.finalize` does no error checking
elseif f === Core.compilerbarrier
na == 2 || return false
return compilerbarrier_nothrow(argtypes[1], nothing)
Expand Down
4 changes: 1 addition & 3 deletions base/gcutils.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license


"""
WeakRef(x)
Expand Down Expand Up @@ -99,8 +98,7 @@ end
Immediately run finalizers registered for object `x`.
"""
finalize(@nospecialize(o)) = ccall(:jl_finalize_th, Cvoid, (Any, Any,),
current_task(), o)
finalize(@nospecialize(o)) = Core.finalize(o)

"""
Base.GC
Expand Down
1 change: 1 addition & 0 deletions src/builtin_proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ DECLARE_BUILTIN(compilerbarrier);
DECLARE_BUILTIN(current_scope);
DECLARE_BUILTIN(donotdelete);
DECLARE_BUILTIN(fieldtype);
DECLARE_BUILTIN(finalize);
DECLARE_BUILTIN(finalizer);
DECLARE_BUILTIN(getfield);
DECLARE_BUILTIN(getglobal);
Expand Down
8 changes: 8 additions & 0 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,13 @@ JL_CALLABLE(jl_f_throw_methoderror)
return jl_nothing;
}

JL_CALLABLE(jl_f_finalize)
{
JL_NARGS(finalize, 1, 1);
jl_finalize(args[0]);
return jl_nothing;
}

JL_CALLABLE(jl_f_ifelse)
{
JL_NARGS(ifelse, 3, 3);
Expand Down Expand Up @@ -2446,6 +2453,7 @@ void jl_init_primitives(void) JL_GC_DISABLED
add_builtin_func("_svec_ref", jl_f__svec_ref);
jl_builtin_current_scope = add_builtin_func("current_scope", jl_f_current_scope);
add_builtin_func("throw_methoderror", jl_f_throw_methoderror);
add_builtin_func("finalize", jl_f_finalize);

// builtin types
add_builtin("Any", (jl_value_t*)jl_any_type);
Expand Down
1 change: 1 addition & 0 deletions src/codegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,7 @@ static const auto &builtin_func_map() {
{ jl_f__call_in_world_total_addr, new JuliaFunction<>{XSTR(jl_f__call_in_world_total), get_func_sig, get_func_attrs} },
{ jl_f_throw_addr, new JuliaFunction<>{XSTR(jl_f_throw), get_func_sig, get_func_attrs} },
{ jl_f_throw_methoderror_addr, new JuliaFunction<>{XSTR(jl_f_throw_methoderror), get_func_sig, get_func_attrs} },
{ jl_f_finalize_addr, new JuliaFunction<>{XSTR(jl_f_finalize), get_func_sig, get_func_attrs} },
{ jl_f_tuple_addr, jltuple_func },
{ jl_f_svec_addr, new JuliaFunction<>{XSTR(jl_f_svec), get_func_sig, get_func_attrs} },
{ jl_f_applicable_addr, new JuliaFunction<>{XSTR(jl_f_applicable), get_func_sig, get_func_attrs} },
Expand Down
2 changes: 1 addition & 1 deletion src/staticdata.c
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ static htable_t relocatable_ext_cis;
// (reverse of fptr_to_id)
// This is a manually constructed dual of the fvars array, which would be produced by codegen for Julia code, for C.
static const jl_fptr_args_t id_to_fptrs[] = {
&jl_f_throw, &jl_f_throw_methoderror, &jl_f_is, &jl_f_typeof, &jl_f_issubtype, &jl_f_isa,
&jl_f_throw, &jl_f_throw_methoderror, &jl_f_finalize, &jl_f_is, &jl_f_typeof, &jl_f_issubtype, &jl_f_isa,
&jl_f_typeassert, &jl_f__apply_iterate, &jl_f__apply_pure,
&jl_f__call_latest, &jl_f__call_in_world, &jl_f__call_in_world_total, &jl_f_isdefined,
&jl_f_tuple, &jl_f_svec, &jl_f_intrinsic_call,
Expand Down
13 changes: 13 additions & 0 deletions test/compiler/inline.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,19 @@ let
@test get_finalization_count() == 1000
end

# early `finalize` insertion
let src = code_typed1((Int,)) do x
xs = finalizer(Ref(x)) do obj
Base.@assume_effects :nothrow :notaskstate
Core.println("finalizing: ", objectid(obj))
end
@show xs[]
return xs[]
end
@test count(iscall((src, Core.finalizer)), src.code) == 1
@test count(iscall((src, Core.finalize)), src.code) == 1
end

# optimize `[push!|pushfirst!](::Vector{Any}, x...)`
@testset "optimize `$f(::Vector{Any}, x...)`" for f = Any[push!, pushfirst!]
@eval begin
Expand Down

0 comments on commit 522c754

Please sign in to comment.