Skip to content

Commit 360bda8

Browse files
committed
optimizer: Julia-level escape analysis
This commit ports [EscapeAnalysis.jl](https://github.com/aviatesk/EscapeAnalysis.jl) into Julia base. You can find the documentation of this escape analysis at [this GitHub page](https://aviatesk.github.io/EscapeAnalysis.jl/dev/)[^1]. [^1]: The same documentation will be included into Julia's developer documentation by this commit. This escape analysis will hopefully be an enabling technology for various memory-related optimizations at Julia's high level compilation pipeline. Possible target optimization includes alias aware SROA (#43888), array SROA (#43909), `mutating_arrayfreeze` optimization (#42465), stack allocation of mutables, finalizer elision and so on[^2]. [^2]: It would be also interesting if LLVM-level optimizations can consume IPO information derived by this escape analysis to broaden optimization possibilities. The primary motivation for porting EA in this PR is to check its impact on latency as well as to get feedbacks from a broader range of developers. The plan is that we first introduce EA in this commit, and then merge the depending PRs built on top of this commit like #43888, #43909 and #42465 This commit simply defines and runs EA inside Julia base compiler and enables the existing test suite with it. In this commit, we just run EA before inlining to generate IPO cache. The depending PRs, EA will be reran after inlining to be used for various local optimizations.
1 parent e3b681c commit 360bda8

21 files changed

+5540
-90
lines changed

base/boot.jl

+32-26
Original file line numberDiff line numberDiff line change
@@ -401,32 +401,38 @@ _new(:QuoteNode, :Any)
401401
_new(:SSAValue, :Int)
402402
_new(:Argument, :Int)
403403
_new(:ReturnNode, :Any)
404-
eval(Core, :(ReturnNode() = $(Expr(:new, :ReturnNode)))) # unassigned val indicates unreachable
405-
eval(Core, :(GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest))))
406-
eval(Core, :(LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing))))
407-
eval(Core, :(LineNumberNode(l::Int, @nospecialize(f)) = $(Expr(:new, :LineNumberNode, :l, :f))))
408-
LineNumberNode(l::Int, f::String) = LineNumberNode(l, Symbol(f))
409-
eval(Core, :(GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s))))
410-
eval(Core, :(SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n))))
411-
eval(Core, :(TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t))))
412-
eval(Core, :(PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values))))
413-
eval(Core, :(PiNode(val, typ) = $(Expr(:new, :PiNode, :val, :typ))))
414-
eval(Core, :(PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values))))
415-
eval(Core, :(UpsilonNode(val) = $(Expr(:new, :UpsilonNode, :val))))
416-
eval(Core, :(UpsilonNode() = $(Expr(:new, :UpsilonNode))))
417-
eval(Core, :(LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int, inlined_at::Int) =
418-
$(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at))))
419-
eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const),
420-
@nospecialize(inferred), const_flags::Int32,
421-
min_world::UInt, max_world::UInt, relocatability::UInt8) =
422-
ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8),
423-
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, relocatability)))
424-
eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))))
425-
eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))))
426-
eval(Core, :(PartialOpaque(@nospecialize(typ), @nospecialize(env), isva::Bool, parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :isva, :parent, :source))))
427-
eval(Core, :(InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype))))
428-
eval(Core, :(MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) =
429-
$(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers))))
404+
eval(Core, quote
405+
ReturnNode() = $(Expr(:new, :ReturnNode)) # unassigned val indicates unreachable
406+
GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest))
407+
LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing))
408+
function LineNumberNode(l::Int, @nospecialize(f))
409+
isa(f, String) && (f = Symbol(f))
410+
return $(Expr(:new, :LineNumberNode, :l, :f))
411+
end
412+
LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int, inlined_at::Int) =
413+
$(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at))
414+
GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s))
415+
SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n))
416+
TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t))
417+
PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values))
418+
PiNode(@nospecialize(val), @nospecialize(typ)) = $(Expr(:new, :PiNode, :val, :typ))
419+
PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values))
420+
UpsilonNode(@nospecialize(val)) = $(Expr(:new, :UpsilonNode, :val))
421+
UpsilonNode() = $(Expr(:new, :UpsilonNode))
422+
function CodeInstance(
423+
mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const),
424+
@nospecialize(inferred), const_flags::Int32, min_world::UInt, max_world::UInt,
425+
relocatability::UInt8, @nospecialize(escapes#=::Union{Nothing,Vector{Core.Compiler.ArgEscapeInfo}}=#))
426+
return ccall(:jl_new_codeinst, Ref{CodeInstance},
427+
(Any, Any, Any, Any, Int32, UInt, UInt, UInt8, Any),
428+
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, relocatability, escapes)
429+
end
430+
Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))
431+
PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))
432+
PartialOpaque(@nospecialize(typ), @nospecialize(env), isva::Bool, parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :isva, :parent, :source))
433+
InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype))
434+
MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) = $(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers))
435+
end)
430436

431437
Module(name::Symbol=:anonymous, std_imports::Bool=true, default_names::Bool=true) = ccall(:jl_f_new_module, Ref{Module}, (Any, Bool, Bool), name, std_imports, default_names)
432438

base/compiler/bootstrap.jl

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ let
1111
world = get_world_counter()
1212
interp = NativeInterpreter(world)
1313

14+
analyze_escapes_tt = Tuple{typeof(analyze_escapes), IRCode, Int, Bool, typeof(code_cache(interp))}
1415
fs = Any[
1516
# we first create caches for the optimizer, because they contain many loop constructions
1617
# and they're better to not run in interpreter even during bootstrapping
17-
run_passes,
18+
analyze_escapes_tt, run_passes,
1819
# then we create caches for inference entries
1920
typeinf_ext, typeinf, typeinf_edge,
2021
]
@@ -32,7 +33,12 @@ let
3233
end
3334
starttime = time()
3435
for f in fs
35-
for m in _methods_by_ftype(Tuple{typeof(f), Vararg{Any}}, 10, typemax(UInt))
36+
if isa(f, DataType) && f.name === typename(Tuple)
37+
matches = _methods_by_ftype(f, 10, typemax(UInt))
38+
else
39+
matches = _methods_by_ftype(Tuple{typeof(f), Vararg{Any}}, 10, typemax(UInt))
40+
end
41+
for m in matches
3642
# remove any TypeVars from the intersection
3743
typ = Any[m.spec_types.parameters...]
3844
for i = 1:length(typ)

base/compiler/compiler.jl

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ ntuple(f, n) = (Any[f(i) for i = 1:n]...,)
9595

9696
# core docsystem
9797
include("docs/core.jl")
98+
import Core.Compiler.CoreDocs
99+
Core.atdoc!(CoreDocs.docm)
98100

99101
# sorting
100102
function sort end

base/compiler/optimize.jl

+54-44
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3+
#############
4+
# constants #
5+
#############
6+
7+
# The slot has uses that are not statically dominated by any assignment
8+
# This is implied by `SLOT_USEDUNDEF`.
9+
# If this is not set, all the uses are (statically) dominated by the defs.
10+
# In particular, if a slot has `AssignedOnce && !StaticUndef`, it is an SSA.
11+
const SLOT_STATICUNDEF = 1 # slot might be used before it is defined (structurally)
12+
const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once
13+
const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError
14+
# const SLOT_CALLED = 64
15+
16+
# NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c
17+
18+
const IR_FLAG_NULL = 0x00
19+
# This statement is marked as @inbounds by user.
20+
# Ff replaced by inlining, any contained boundschecks may be removed.
21+
const IR_FLAG_INBOUNDS = 0x01 << 0
22+
# This statement is marked as @inline by user
23+
const IR_FLAG_INLINE = 0x01 << 1
24+
# This statement is marked as @noinline by user
25+
const IR_FLAG_NOINLINE = 0x01 << 2
26+
const IR_FLAG_THROW_BLOCK = 0x01 << 3
27+
# This statement may be removed if its result is unused. In particular it must
28+
# thus be both pure and effect free.
29+
const IR_FLAG_EFFECT_FREE = 0x01 << 4
30+
31+
# known to be always effect-free (in particular nothrow)
32+
const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields]
33+
34+
# known to be effect-free if the are nothrow
35+
const _PURE_OR_ERROR_BUILTINS = [
36+
fieldtype, apply_type, isa, UnionAll,
37+
getfield, arrayref, const_arrayref, arraysize, isdefined, Core.sizeof,
38+
Core.kwfunc, Core.ifelse, Core._typevar, (<:)
39+
]
40+
41+
const TOP_TUPLE = GlobalRef(Core, :tuple)
42+
343
#####################
444
# OptimizationState #
545
#####################
@@ -21,10 +61,10 @@ function push!(et::EdgeTracker, ci::CodeInstance)
2161
push!(et, ci.def)
2262
end
2363

24-
struct InliningState{S <: Union{EdgeTracker, Nothing}, T, I<:AbstractInterpreter}
64+
struct InliningState{S <: Union{EdgeTracker, Nothing}, MICache, I<:AbstractInterpreter}
2565
params::OptimizationParams
2666
et::S
27-
mi_cache::T
67+
mi_cache::MICache # TODO move this to `OptimizationState` (as used by EscapeAnalysis as well)
2868
interp::I
2969
end
3070

@@ -52,7 +92,12 @@ function inlining_policy(interp::AbstractInterpreter, @nospecialize(src), stmt_f
5292
return nothing
5393
end
5494

95+
function argextype end # imported by EscapeAnalysis
96+
function stmt_effect_free end # imported by EscapeAnalysis
97+
function alloc_array_ndims end # imported by EscapeAnalysis
5598
include("compiler/ssair/driver.jl")
99+
using .EscapeAnalysis
100+
import .EscapeAnalysis: EscapeState
56101

57102
mutable struct OptimizationState
58103
linfo::MethodInstance
@@ -121,46 +166,6 @@ function ir_to_codeinf!(opt::OptimizationState)
121166
return src
122167
end
123168

124-
#############
125-
# constants #
126-
#############
127-
128-
# The slot has uses that are not statically dominated by any assignment
129-
# This is implied by `SLOT_USEDUNDEF`.
130-
# If this is not set, all the uses are (statically) dominated by the defs.
131-
# In particular, if a slot has `AssignedOnce && !StaticUndef`, it is an SSA.
132-
const SLOT_STATICUNDEF = 1 # slot might be used before it is defined (structurally)
133-
const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once
134-
const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError
135-
# const SLOT_CALLED = 64
136-
137-
# NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c
138-
139-
const IR_FLAG_NULL = 0x00
140-
# This statement is marked as @inbounds by user.
141-
# Ff replaced by inlining, any contained boundschecks may be removed.
142-
const IR_FLAG_INBOUNDS = 0x01 << 0
143-
# This statement is marked as @inline by user
144-
const IR_FLAG_INLINE = 0x01 << 1
145-
# This statement is marked as @noinline by user
146-
const IR_FLAG_NOINLINE = 0x01 << 2
147-
const IR_FLAG_THROW_BLOCK = 0x01 << 3
148-
# This statement may be removed if its result is unused. In particular it must
149-
# thus be both pure and effect free.
150-
const IR_FLAG_EFFECT_FREE = 0x01 << 4
151-
152-
# known to be always effect-free (in particular nothrow)
153-
const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields]
154-
155-
# known to be effect-free if the are nothrow
156-
const _PURE_OR_ERROR_BUILTINS = [
157-
fieldtype, apply_type, isa, UnionAll,
158-
getfield, arrayref, const_arrayref, arraysize, isdefined, Core.sizeof,
159-
Core.kwfunc, Core.ifelse, Core._typevar, (<:)
160-
]
161-
162-
const TOP_TUPLE = GlobalRef(Core, :tuple)
163-
164169
#########
165170
# logic #
166171
#########
@@ -505,18 +510,23 @@ end
505510
# run the optimization work
506511
function optimize(interp::AbstractInterpreter, opt::OptimizationState,
507512
params::OptimizationParams, caller::InferenceResult)
508-
@timeit "optimizer" ir = run_passes(opt.src, opt)
513+
@timeit "optimizer" ir = run_passes(opt.src, opt, caller)
509514
return finish(interp, opt, params, ir, caller)
510515
end
511516

512-
function run_passes(ci::CodeInfo, sv::OptimizationState)
517+
function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult)
513518
@timeit "convert" ir = convert_to_ircode(ci, sv)
514519
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)
515520
# TODO: Domsorting can produce an updated domtree - no need to recompute here
516521
@timeit "compact 1" ir = compact!(ir)
522+
nargs = let def = sv.linfo.def; isa(def, Method) ? Int(def.nargs) : 0; end
523+
@timeit "IPO EA" state, callinfo = analyze_escapes(ir, nargs, false, sv.inlining.mi_cache)
524+
cache_escapes!(caller, state, ir)
517525
@timeit "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds)
518526
# @timeit "verify 2" verify_ir(ir)
519527
@timeit "compact 2" ir = compact!(ir)
528+
@timeit "Local EA" state, callinfo = analyze_escapes(ir, nargs, true, sv.inlining.mi_cache)
529+
@assert callinfo === nothing
520530
@timeit "SROA" ir = sroa_pass!(ir)
521531
@timeit "ADCE" ir = adce_pass!(ir)
522532
@timeit "type lift" ir = type_lift_pass!(ir)

0 commit comments

Comments
 (0)