Skip to content

Commit d1eb45c

Browse files
aviateskLilithHafner
authored andcommitted
optimizer: Julia-level escape analysis (JuliaLang#43800)
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 (JuliaLang#43888), array SROA (JuliaLang#43909), `mutating_arrayfreeze` optimization (JuliaLang#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 by 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 to Julia Base by this commit, and then merge the depending PRs built on top of this commit later. This commit simply defines EA inside Julia base compiler and enables the existing test suite with it. In this commit we don't run EA at all, and so this commit shouldn't affect Julia-level compilation latency. In the depending PRs, EA will run in two stages: - `IPO EA`: run EA on pre-inlining state to generate IPO-valid cache - `Local EA`: run EA on post-inlining state to generate local escape information used for various optimizations In order to integrate `IPO EA` with our compilation cache system, this commit also implements a new `CodeInstance.argescapes` field that keeps the IPO-valid cache generated by `IPO EA`.
1 parent 5342857 commit d1eb45c

24 files changed

+5690
-96
lines changed

base/boot.jl

+41-34
Original file line numberDiff line numberDiff line change
@@ -394,40 +394,47 @@ struct VecElement{T}
394394
end
395395
VecElement(arg::T) where {T} = VecElement{T}(arg)
396396

397-
_new(typ::Symbol, argty::Symbol) = eval(Core, :($typ(@nospecialize n::$argty) = $(Expr(:new, typ, :n))))
398-
_new(:GotoNode, :Int)
399-
_new(:NewvarNode, :SlotNumber)
400-
_new(:QuoteNode, :Any)
401-
_new(:SSAValue, :Int)
402-
_new(:Argument, :Int)
403-
_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, ipo_effects::UInt8, effects::UInt8,
422-
relocatability::UInt8) =
423-
ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8, UInt8, UInt8),
424-
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, ipo_effects, effects, relocatability)))
425-
eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))))
426-
eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))))
427-
eval(Core, :(PartialOpaque(@nospecialize(typ), @nospecialize(env), parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :parent, :source))))
428-
eval(Core, :(InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype))))
429-
eval(Core, :(MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) =
430-
$(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers))))
397+
eval(Core, quote
398+
GotoNode(label::Int) = $(Expr(:new, :GotoNode, :label))
399+
NewvarNode(slot::SlotNumber) = $(Expr(:new, :NewvarNode, :slot))
400+
QuoteNode(@nospecialize value) = $(Expr(:new, :QuoteNode, :value))
401+
SSAValue(id::Int) = $(Expr(:new, :SSAValue, :id))
402+
Argument(n::Int) = $(Expr(:new, :Argument, :n))
403+
ReturnNode(@nospecialize val) = $(Expr(:new, :ReturnNode, :val))
404+
ReturnNode() = $(Expr(:new, :ReturnNode)) # unassigned val indicates unreachable
405+
GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest))
406+
LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing))
407+
function LineNumberNode(l::Int, @nospecialize(f))
408+
isa(f, String) && (f = Symbol(f))
409+
return $(Expr(:new, :LineNumberNode, :l, :f))
410+
end
411+
LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int, inlined_at::Int) =
412+
$(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at))
413+
GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s))
414+
SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n))
415+
TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t))
416+
PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values))
417+
PiNode(@nospecialize(val), @nospecialize(typ)) = $(Expr(:new, :PiNode, :val, :typ))
418+
PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values))
419+
UpsilonNode(@nospecialize(val)) = $(Expr(:new, :UpsilonNode, :val))
420+
UpsilonNode() = $(Expr(:new, :UpsilonNode))
421+
function CodeInstance(
422+
mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const),
423+
@nospecialize(inferred), const_flags::Int32, min_world::UInt, max_world::UInt,
424+
ipo_effects::UInt8, effects::UInt8, @nospecialize(argescapes#=::Union{Nothing,Vector{ArgEscapeInfo}}=#),
425+
relocatability::UInt8)
426+
return ccall(:jl_new_codeinst, Ref{CodeInstance},
427+
(Any, Any, Any, Any, Int32, UInt, UInt, UInt8, UInt8, Any, UInt8),
428+
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world,
429+
ipo_effects, effects, argescapes,
430+
relocatability)
431+
end
432+
Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))
433+
PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))
434+
PartialOpaque(@nospecialize(typ), @nospecialize(env), parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :parent, :source))
435+
InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype))
436+
MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) = $(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers))
437+
end)
431438

432439
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)
433440

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(null_escape_cache)}
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+
tt = f
38+
else
39+
tt = Tuple{typeof(f), Vararg{Any}}
40+
end
41+
for m in _methods_by_ftype(tt, 10, typemax(UInt))
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
@@ -98,6 +98,8 @@ ntuple(f, n) = (Any[f(i) for i = 1:n]...,)
9898

9999
# core docsystem
100100
include("docs/core.jl")
101+
import Core.Compiler.CoreDocs
102+
Core.atdoc!(CoreDocs.docm)
101103

102104
# sorting
103105
function sort end

base/compiler/optimize.jl

+60-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
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+
const TOP_TUPLE = GlobalRef(Core, :tuple)
32+
333
#####################
434
# OptimizationState #
535
#####################
@@ -21,10 +51,10 @@ function push!(et::EdgeTracker, ci::CodeInstance)
2151
push!(et, ci.def)
2252
end
2353

24-
struct InliningState{S <: Union{EdgeTracker, Nothing}, T, I<:AbstractInterpreter}
54+
struct InliningState{S <: Union{EdgeTracker, Nothing}, MICache, I<:AbstractInterpreter}
2555
params::OptimizationParams
2656
et::S
27-
mi_cache::T
57+
mi_cache::MICache # TODO move this to `OptimizationState` (as used by EscapeAnalysis as well)
2858
interp::I
2959
end
3060

@@ -121,36 +151,6 @@ function ir_to_codeinf!(opt::OptimizationState)
121151
return src
122152
end
123153

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-
const TOP_TUPLE = GlobalRef(Core, :tuple)
153-
154154
#########
155155
# logic #
156156
#########
@@ -502,11 +502,37 @@ end
502502
# run the optimization work
503503
function optimize(interp::AbstractInterpreter, opt::OptimizationState,
504504
params::OptimizationParams, caller::InferenceResult)
505-
@timeit "optimizer" ir = run_passes(opt.src, opt)
505+
@timeit "optimizer" ir = run_passes(opt.src, opt, caller)
506506
return finish(interp, opt, params, ir, caller)
507507
end
508508

509-
function run_passes(ci::CodeInfo, sv::OptimizationState)
509+
using .EscapeAnalysis
510+
import .EscapeAnalysis: EscapeState, ArgEscapeCache, is_ipo_profitable
511+
512+
"""
513+
cache_escapes!(caller::InferenceResult, estate::EscapeState)
514+
515+
Transforms escape information of call arguments of `caller`,
516+
and then caches it into a global cache for later interprocedural propagation.
517+
"""
518+
cache_escapes!(caller::InferenceResult, estate::EscapeState) =
519+
caller.argescapes = ArgEscapeCache(estate)
520+
521+
function ipo_escape_cache(mi_cache::MICache) where MICache
522+
return function (linfo::Union{InferenceResult,MethodInstance})
523+
if isa(linfo, InferenceResult)
524+
argescapes = linfo.argescapes
525+
else
526+
codeinst = get(mi_cache, linfo, nothing)
527+
isa(codeinst, CodeInstance) || return nothing
528+
argescapes = codeinst.argescapes
529+
end
530+
return argescapes !== nothing ? argescapes::ArgEscapeCache : nothing
531+
end
532+
end
533+
null_escape_cache(linfo::Union{InferenceResult,MethodInstance}) = nothing
534+
535+
function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult)
510536
@timeit "convert" ir = convert_to_ircode(ci, sv)
511537
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)
512538
# TODO: Domsorting can produce an updated domtree - no need to recompute here

0 commit comments

Comments
 (0)