Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimizer: Julia-level escape analysis #43800

Merged
merged 1 commit into from
Feb 16, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 41 additions & 34 deletions base/boot.jl
Original file line number Diff line number Diff line change
@@ -394,40 +394,47 @@ struct VecElement{T}
end
VecElement(arg::T) where {T} = VecElement{T}(arg)

_new(typ::Symbol, argty::Symbol) = eval(Core, :($typ(@nospecialize n::$argty) = $(Expr(:new, typ, :n))))
_new(:GotoNode, :Int)
_new(:NewvarNode, :SlotNumber)
_new(:QuoteNode, :Any)
_new(:SSAValue, :Int)
_new(:Argument, :Int)
_new(:ReturnNode, :Any)
eval(Core, :(ReturnNode() = $(Expr(:new, :ReturnNode)))) # unassigned val indicates unreachable
eval(Core, :(GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest))))
eval(Core, :(LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing))))
eval(Core, :(LineNumberNode(l::Int, @nospecialize(f)) = $(Expr(:new, :LineNumberNode, :l, :f))))
LineNumberNode(l::Int, f::String) = LineNumberNode(l, Symbol(f))
eval(Core, :(GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s))))
eval(Core, :(SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n))))
eval(Core, :(TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t))))
eval(Core, :(PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values))))
eval(Core, :(PiNode(val, typ) = $(Expr(:new, :PiNode, :val, :typ))))
eval(Core, :(PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values))))
eval(Core, :(UpsilonNode(val) = $(Expr(:new, :UpsilonNode, :val))))
eval(Core, :(UpsilonNode() = $(Expr(:new, :UpsilonNode))))
eval(Core, :(LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int, inlined_at::Int) =
$(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at))))
eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const),
@nospecialize(inferred), const_flags::Int32,
min_world::UInt, max_world::UInt, ipo_effects::UInt8, effects::UInt8,
relocatability::UInt8) =
ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8, UInt8, UInt8),
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, ipo_effects, effects, relocatability)))
eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))))
eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))))
eval(Core, :(PartialOpaque(@nospecialize(typ), @nospecialize(env), parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :parent, :source))))
eval(Core, :(InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype))))
eval(Core, :(MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) =
$(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers))))
eval(Core, quote
GotoNode(label::Int) = $(Expr(:new, :GotoNode, :label))
NewvarNode(slot::SlotNumber) = $(Expr(:new, :NewvarNode, :slot))
QuoteNode(@nospecialize value) = $(Expr(:new, :QuoteNode, :value))
SSAValue(id::Int) = $(Expr(:new, :SSAValue, :id))
Argument(n::Int) = $(Expr(:new, :Argument, :n))
ReturnNode(@nospecialize val) = $(Expr(:new, :ReturnNode, :val))
ReturnNode() = $(Expr(:new, :ReturnNode)) # unassigned val indicates unreachable
GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest))
LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing))
function LineNumberNode(l::Int, @nospecialize(f))
isa(f, String) && (f = Symbol(f))
return $(Expr(:new, :LineNumberNode, :l, :f))
end
LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int, inlined_at::Int) =
$(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at))
GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s))
SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n))
TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t))
PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values))
PiNode(@nospecialize(val), @nospecialize(typ)) = $(Expr(:new, :PiNode, :val, :typ))
PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values))
UpsilonNode(@nospecialize(val)) = $(Expr(:new, :UpsilonNode, :val))
UpsilonNode() = $(Expr(:new, :UpsilonNode))
function CodeInstance(
mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const),
@nospecialize(inferred), const_flags::Int32, min_world::UInt, max_world::UInt,
ipo_effects::UInt8, effects::UInt8, @nospecialize(argescapes#=::Union{Nothing,Vector{ArgEscapeInfo}}=#),
relocatability::UInt8)
return ccall(:jl_new_codeinst, Ref{CodeInstance},
(Any, Any, Any, Any, Int32, UInt, UInt, UInt8, UInt8, Any, UInt8),
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world,
ipo_effects, effects, argescapes,
relocatability)
end
Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))
PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))
PartialOpaque(@nospecialize(typ), @nospecialize(env), parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :parent, :source))
InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype))
MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) = $(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers))
end)

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)

10 changes: 8 additions & 2 deletions base/compiler/bootstrap.jl
Original file line number Diff line number Diff line change
@@ -11,10 +11,11 @@ let
world = get_world_counter()
interp = NativeInterpreter(world)

analyze_escapes_tt = Tuple{typeof(analyze_escapes), IRCode, Int, Bool, typeof(null_escape_cache)}
fs = Any[
# we first create caches for the optimizer, because they contain many loop constructions
# and they're better to not run in interpreter even during bootstrapping
run_passes,
#=analyze_escapes_tt,=# run_passes,
# then we create caches for inference entries
typeinf_ext, typeinf, typeinf_edge,
]
@@ -32,7 +33,12 @@ let
end
starttime = time()
for f in fs
for m in _methods_by_ftype(Tuple{typeof(f), Vararg{Any}}, 10, typemax(UInt))
if isa(f, DataType) && f.name === typename(Tuple)
tt = f
else
tt = Tuple{typeof(f), Vararg{Any}}
end
for m in _methods_by_ftype(tt, 10, typemax(UInt))
# remove any TypeVars from the intersection
typ = Any[m.spec_types.parameters...]
for i = 1:length(typ)
2 changes: 2 additions & 0 deletions base/compiler/compiler.jl
Original file line number Diff line number Diff line change
@@ -98,6 +98,8 @@ ntuple(f, n) = (Any[f(i) for i = 1:n]...,)

# core docsystem
include("docs/core.jl")
import Core.Compiler.CoreDocs
Core.atdoc!(CoreDocs.docm)

# sorting
function sort end
94 changes: 60 additions & 34 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

#############
# constants #
#############

# The slot has uses that are not statically dominated by any assignment
# This is implied by `SLOT_USEDUNDEF`.
# If this is not set, all the uses are (statically) dominated by the defs.
# In particular, if a slot has `AssignedOnce && !StaticUndef`, it is an SSA.
const SLOT_STATICUNDEF = 1 # slot might be used before it is defined (structurally)
const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do 0x01 << 0

const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError
# const SLOT_CALLED = 64

# NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c

const IR_FLAG_NULL = 0x00
# This statement is marked as @inbounds by user.
# Ff replaced by inlining, any contained boundschecks may be removed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ff?

const IR_FLAG_INBOUNDS = 0x01 << 0
# This statement is marked as @inline by user
const IR_FLAG_INLINE = 0x01 << 1
# This statement is marked as @noinline by user
const IR_FLAG_NOINLINE = 0x01 << 2
const IR_FLAG_THROW_BLOCK = 0x01 << 3
# This statement may be removed if its result is unused. In particular it must
# thus be both pure and effect free.
const IR_FLAG_EFFECT_FREE = 0x01 << 4

const TOP_TUPLE = GlobalRef(Core, :tuple)

#####################
# OptimizationState #
#####################
@@ -21,10 +51,10 @@ function push!(et::EdgeTracker, ci::CodeInstance)
push!(et, ci.def)
end

struct InliningState{S <: Union{EdgeTracker, Nothing}, T, I<:AbstractInterpreter}
struct InliningState{S <: Union{EdgeTracker, Nothing}, MICache, I<:AbstractInterpreter}
params::OptimizationParams
et::S
mi_cache::T
mi_cache::MICache # TODO move this to `OptimizationState` (as used by EscapeAnalysis as well)
interp::I
end

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

#############
# constants #
#############

# The slot has uses that are not statically dominated by any assignment
# This is implied by `SLOT_USEDUNDEF`.
# If this is not set, all the uses are (statically) dominated by the defs.
# In particular, if a slot has `AssignedOnce && !StaticUndef`, it is an SSA.
const SLOT_STATICUNDEF = 1 # slot might be used before it is defined (structurally)
const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once
const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError
# const SLOT_CALLED = 64

# NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c

const IR_FLAG_NULL = 0x00
# This statement is marked as @inbounds by user.
# Ff replaced by inlining, any contained boundschecks may be removed.
const IR_FLAG_INBOUNDS = 0x01 << 0
# This statement is marked as @inline by user
const IR_FLAG_INLINE = 0x01 << 1
# This statement is marked as @noinline by user
const IR_FLAG_NOINLINE = 0x01 << 2
const IR_FLAG_THROW_BLOCK = 0x01 << 3
# This statement may be removed if its result is unused. In particular it must
# thus be both pure and effect free.
const IR_FLAG_EFFECT_FREE = 0x01 << 4

const TOP_TUPLE = GlobalRef(Core, :tuple)

#########
# logic #
#########
@@ -502,11 +502,37 @@ end
# run the optimization work
function optimize(interp::AbstractInterpreter, opt::OptimizationState,
params::OptimizationParams, caller::InferenceResult)
@timeit "optimizer" ir = run_passes(opt.src, opt)
@timeit "optimizer" ir = run_passes(opt.src, opt, caller)
return finish(interp, opt, params, ir, caller)
end

function run_passes(ci::CodeInfo, sv::OptimizationState)
using .EscapeAnalysis
import .EscapeAnalysis: EscapeState, ArgEscapeCache, is_ipo_profitable

"""
cache_escapes!(caller::InferenceResult, estate::EscapeState)

Transforms escape information of call arguments of `caller`,
and then caches it into a global cache for later interprocedural propagation.
"""
cache_escapes!(caller::InferenceResult, estate::EscapeState) =
caller.argescapes = ArgEscapeCache(estate)

function ipo_escape_cache(mi_cache::MICache) where MICache
return function (linfo::Union{InferenceResult,MethodInstance})
if isa(linfo, InferenceResult)
argescapes = linfo.argescapes
else
codeinst = get(mi_cache, linfo, nothing)
isa(codeinst, CodeInstance) || return nothing
argescapes = codeinst.argescapes
end
return argescapes !== nothing ? argescapes::ArgEscapeCache : nothing
end
end
null_escape_cache(linfo::Union{InferenceResult,MethodInstance}) = nothing

function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult)
@timeit "convert" ir = convert_to_ircode(ci, sv)
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)
# TODO: Domsorting can produce an updated domtree - no need to recompute here
1,913 changes: 1,913 additions & 0 deletions base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl

Large diffs are not rendered by default.

143 changes: 143 additions & 0 deletions base/compiler/ssair/EscapeAnalysis/disjoint_set.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# A disjoint set implementation adapted from
# https://github.com/JuliaCollections/DataStructures.jl/blob/f57330a3b46f779b261e6c07f199c88936f28839/src/disjoint_set.jl
# under the MIT license: https://github.com/JuliaCollections/DataStructures.jl/blob/master/License.md

# imports
import ._TOP_MOD:
length,
eltype,
union!,
push!
# usings
import ._TOP_MOD:
OneTo, collect, zero, zeros, one, typemax

# Disjoint-Set

############################################################
#
# A forest of disjoint sets of integers
#
# Since each element is an integer, we can use arrays
# instead of dictionary (for efficiency)
#
# Disjoint sets over other key types can be implemented
# based on an IntDisjointSet through a map from the key
# to an integer index
#
############################################################

_intdisjointset_bounds_err_msg(T) = "the maximum number of elements in IntDisjointSet{$T} is $(typemax(T))"

"""
IntDisjointSet{T<:Integer}(n::Integer)
A forest of disjoint sets of integers, which is a data structure
(also called a union–find data structure or merge–find set)
that tracks a set of elements partitioned
into a number of disjoint (non-overlapping) subsets.
"""
mutable struct IntDisjointSet{T<:Integer}
parents::Vector{T}
ranks::Vector{T}
ngroups::T
end

IntDisjointSet(n::T) where {T<:Integer} = IntDisjointSet{T}(collect(OneTo(n)), zeros(T, n), n)
IntDisjointSet{T}(n::Integer) where {T<:Integer} = IntDisjointSet{T}(collect(OneTo(T(n))), zeros(T, T(n)), T(n))
length(s::IntDisjointSet) = length(s.parents)

"""
num_groups(s::IntDisjointSet)
Get a number of groups.
"""
num_groups(s::IntDisjointSet) = s.ngroups
eltype(::Type{IntDisjointSet{T}}) where {T<:Integer} = T

# find the root element of the subset that contains x
# path compression is implemented here
function find_root_impl!(parents::Vector{T}, x::Integer) where {T<:Integer}
p = parents[x]
@inbounds if parents[p] != p
parents[x] = p = _find_root_impl!(parents, p)
end
return p
end

# unsafe version of the above
function _find_root_impl!(parents::Vector{T}, x::Integer) where {T<:Integer}
@inbounds p = parents[x]
@inbounds if parents[p] != p
parents[x] = p = _find_root_impl!(parents, p)
end
return p
end

"""
find_root!(s::IntDisjointSet{T}, x::T)
Find the root element of the subset that contains an member `x`.
Path compression happens here.
"""
find_root!(s::IntDisjointSet{T}, x::T) where {T<:Integer} = find_root_impl!(s.parents, x)

"""
in_same_set(s::IntDisjointSet{T}, x::T, y::T)
Returns `true` if `x` and `y` belong to the same subset in `s`, and `false` otherwise.
"""
in_same_set(s::IntDisjointSet{T}, x::T, y::T) where {T<:Integer} = find_root!(s, x) == find_root!(s, y)

"""
union!(s::IntDisjointSet{T}, x::T, y::T)
Merge the subset containing `x` and that containing `y` into one
and return the root of the new set.
"""
function union!(s::IntDisjointSet{T}, x::T, y::T) where {T<:Integer}
parents = s.parents
xroot = find_root_impl!(parents, x)
yroot = find_root_impl!(parents, y)
return xroot != yroot ? root_union!(s, xroot, yroot) : xroot
end

"""
root_union!(s::IntDisjointSet{T}, x::T, y::T)
Form a new set that is the union of the two sets whose root elements are
`x` and `y` and return the root of the new set.
Assume `x ≠ y` (unsafe).
"""
function root_union!(s::IntDisjointSet{T}, x::T, y::T) where {T<:Integer}
parents = s.parents
rks = s.ranks
@inbounds xrank = rks[x]
@inbounds yrank = rks[y]

if xrank < yrank
x, y = y, x
elseif xrank == yrank
rks[x] += one(T)
end
@inbounds parents[y] = x
s.ngroups -= one(T)
return x
end

"""
push!(s::IntDisjointSet{T})
Make a new subset with an automatically chosen new element `x`.
Returns the new element. Throw an `ArgumentError` if the
capacity of the set would be exceeded.
"""
function push!(s::IntDisjointSet{T}) where {T<:Integer}
l = length(s)
l < typemax(T) || throw(ArgumentError(_intdisjointset_bounds_err_msg(T)))
x = l + one(T)
push!(s.parents, x)
push!(s.ranks, zero(T))
s.ngroups += one(T)
return x
end
151 changes: 151 additions & 0 deletions base/compiler/ssair/EscapeAnalysis/interprocedural.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# TODO this file contains many duplications with the inlining analysis code, factor them out

import Core.Compiler:
MethodInstance, InferenceResult, Signature, ConstResult,
MethodResultPure, MethodMatchInfo, UnionSplitInfo, ConstCallInfo, InvokeCallInfo,
call_sig, argtypes_to_type, is_builtin, is_return_type, istopfunction, validate_sparams,
specialize_method, invoke_rewrite

const Linfo = Union{MethodInstance,InferenceResult}
struct CallInfo
linfos::Vector{Linfo}
nothrow::Bool
end

function resolve_call(ir::IRCode, stmt::Expr, @nospecialize(info))
sig = call_sig(ir, stmt)
if sig === nothing
return missing
end
# TODO handle _apply_iterate
if is_builtin(sig) && sig.f !== invoke
return false
end
# handling corresponding to late_inline_special_case!
(; f, argtypes) = sig
if length(argtypes) == 3 && istopfunction(f, :!==)
return true
elseif length(argtypes) == 3 && istopfunction(f, :(>:))
return true
elseif f === TypeVar && 2 length(argtypes) 4 && (argtypes[2] Symbol)
return true
elseif f === UnionAll && length(argtypes) == 3 && (argtypes[2] TypeVar)
return true
elseif is_return_type(f)
return true
end
if info isa MethodResultPure
return true
elseif info === false
return missing
end
# TODO handle OpaqueClosureCallInfo
if sig.f === invoke
isa(info, InvokeCallInfo) || return missing
return analyze_invoke_call(sig, info)
elseif isa(info, ConstCallInfo)
return analyze_const_call(sig, info)
elseif isa(info, MethodMatchInfo)
infos = MethodMatchInfo[info]
elseif isa(info, UnionSplitInfo)
infos = info.matches
else # isa(info, ReturnTypeCallInfo), etc.
return missing
end
return analyze_call(sig, infos)
end

function analyze_invoke_call(sig::Signature, info::InvokeCallInfo)
match = info.match
if !match.fully_covers
# TODO: We could union split out the signature check and continue on
return missing
end
result = info.result
if isa(result, InferenceResult)
return CallInfo(Linfo[result], true)
else
argtypes = invoke_rewrite(sig.argtypes)
mi = analyze_match(match, length(argtypes))
mi === nothing && return missing
return CallInfo(Linfo[mi], true)
end
end

function analyze_const_call(sig::Signature, cinfo::ConstCallInfo)
linfos = Linfo[]
(; call, results) = cinfo
infos = isa(call, MethodMatchInfo) ? MethodMatchInfo[call] : call.matches
local nothrow = true # required to account for potential escape via MethodError
local j = 0
for i in 1:length(infos)
meth = infos[i].results
nothrow &= !meth.ambig
nmatch = Core.Compiler.length(meth)
if nmatch == 0 # No applicable methods
# mark this call may potentially throw, and the try next union split
nothrow = false
continue
end
for i = 1:nmatch
j += 1
result = results[j]
match = Core.Compiler.getindex(meth, i)
if result === nothing
mi = analyze_match(match, length(sig.argtypes))
mi === nothing && return missing
push!(linfos, mi)
elseif isa(result, ConstResult)
# TODO we may want to feedback information that this call always throws if !isdefined(result, :result)
push!(linfos, result.mi)
else
push!(linfos, result)
end
nothrow &= match.fully_covers
end
end
return CallInfo(linfos, nothrow)
end

function analyze_call(sig::Signature, infos::Vector{MethodMatchInfo})
linfos = Linfo[]
local nothrow = true # required to account for potential escape via MethodError
for i in 1:length(infos)
meth = infos[i].results
nothrow &= !meth.ambig
nmatch = Core.Compiler.length(meth)
if nmatch == 0 # No applicable methods
# mark this call may potentially throw, and the try next union split
nothrow = false
continue
end
for i = 1:nmatch
match = Core.Compiler.getindex(meth, i)
mi = analyze_match(match, length(sig.argtypes))
mi === nothing && return missing
push!(linfos, mi)
nothrow &= match.fully_covers
end
end
return CallInfo(linfos, nothrow)
end

function analyze_match(match::MethodMatch, npassedargs::Int)
method = match.method
na = Int(method.nargs)
if na != npassedargs && !(na > 0 && method.isva)
# we have a method match only because an earlier
# inference step shortened our call args list, even
# though we have too many arguments to actually
# call this function
return nothing
end

# Bail out if any static parameters are left as TypeVar
# COMBAK is this needed for escape analysis?
validate_sparams(match.sparams) || return nothing

# See if there exists a specialization for this method signature
mi = specialize_method(match; preexisting=true) # Union{Nothing, MethodInstance}
return mi
end
11 changes: 7 additions & 4 deletions base/compiler/ssair/driver.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

using Core: LineInfoNode

if false
import Base: Base, @show
else
@@ -10,12 +8,17 @@ else
end
end

function argextype end # imported by EscapeAnalysis
function stmt_effect_free end # imported by EscapeAnalysis
function alloc_array_ndims end # imported by EscapeAnalysis
function try_compute_field end # imported by EscapeAnalysis

include("compiler/ssair/basicblock.jl")
include("compiler/ssair/domtree.jl")
include("compiler/ssair/ir.jl")
include("compiler/ssair/slot2ssa.jl")
include("compiler/ssair/passes.jl")
include("compiler/ssair/inlining.jl")
include("compiler/ssair/verify.jl")
include("compiler/ssair/legacy.jl")
#@isdefined(Base) && include("compiler/ssair/show.jl")
include("compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl")
include("compiler/ssair/passes.jl")
2 changes: 1 addition & 1 deletion base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
@@ -1288,7 +1288,7 @@ function apply_type_nothrow(argtypes::Array{Any, 1}, @nospecialize(rt))
return false
end
elseif (isa(ai, Const) && isa(ai.val, Type)) || isconstType(ai)
ai = isa(ai, Const) ? ai.val : ai.parameters[1]
ai = isa(ai, Const) ? ai.val : (ai::DataType).parameters[1]
if has_free_typevars(u.var.lb) || has_free_typevars(u.var.ub)
return false
end
2 changes: 1 addition & 1 deletion base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
@@ -313,7 +313,7 @@ function CodeInstance(
widenconst(result_type), rettype_const, inferred_result,
const_flags, first(valid_worlds), last(valid_worlds),
# TODO: Actually do something with non-IPO effects
encode_effects(result.ipo_effects), encode_effects(result.ipo_effects),
encode_effects(result.ipo_effects), encode_effects(result.ipo_effects), result.argescapes,
relocatability)
end

12 changes: 7 additions & 5 deletions base/compiler/types.jl
Original file line number Diff line number Diff line change
@@ -120,15 +120,17 @@ mutable struct InferenceResult
linfo::MethodInstance
argtypes::Vector{Any}
overridden_by_const::BitVector
result # ::Type, or InferenceState if WIP
src #::Union{CodeInfo, OptimizationState, Nothing} # if inferred copy is available
result # ::Type, or InferenceState if WIP
src # ::Union{CodeInfo, OptimizationState} if inferred copy is available, nothing otherwise
valid_worlds::WorldRange # if inference and optimization is finished
ipo_effects::Effects # if inference is finished
effects::Effects # if optimization is finished
ipo_effects::Effects # if inference is finished
effects::Effects # if optimization is finished
argescapes # ::ArgEscapeCache if optimized, nothing otherwise
function InferenceResult(linfo::MethodInstance,
arginfo#=::Union{Nothing,Tuple{ArgInfo,InferenceState}}=# = nothing)
argtypes, overridden_by_const = matching_cache_argtypes(linfo, arginfo)
return new(linfo, argtypes, overridden_by_const, Any, nothing, WorldRange(), Effects(), Effects())
return new(linfo, argtypes, overridden_by_const, Any, nothing,
WorldRange(), Effects(), Effects(), nothing)
end
end

4 changes: 4 additions & 0 deletions base/compiler/utilities.jl
Original file line number Diff line number Diff line change
@@ -19,13 +19,17 @@ function _any(@nospecialize(f), a)
end
return false
end
any(@nospecialize(f), itr) = _any(f, itr)
any(itr) = _any(identity, itr)

function _all(@nospecialize(f), a)
for x in a
f(x) || return false
end
return true
end
all(@nospecialize(f), itr) = _all(f, itr)
all(itr) = _all(identity, itr)

function contains_is(itr, @nospecialize(x))
for y in itr
1 change: 1 addition & 0 deletions doc/make.jl
Original file line number Diff line number Diff line change
@@ -148,6 +148,7 @@ DevDocs = [
"devdocs/require.md",
"devdocs/inference.md",
"devdocs/ssair.md",
"devdocs/EscapeAnalysis.md",
"devdocs/gc-sa.md",
],
"Developing/debugging Julia's C code" => [
398 changes: 398 additions & 0 deletions doc/src/devdocs/EscapeAnalysis.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion doc/src/devdocs/llvm.md
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ The difference between an intrinsic and a builtin is that a builtin is a first c
that can be used like any other Julia function. An intrinsic can operate only on unboxed data,
and therefore its arguments must be statically typed.

### Alias Analysis
### [Alias Analysis](@id LLVM-Alias-Analysis)

Julia currently uses LLVM's [Type Based Alias Analysis](https://llvm.org/docs/LangRef.html#tbaa-metadata).
To find the comments that document the inclusion relationships, look for `static MDNode*` in
4 changes: 4 additions & 0 deletions src/dump.c
Original file line number Diff line number Diff line change
@@ -524,12 +524,14 @@ static void jl_serialize_code_instance(jl_serializer_state *s, jl_code_instance_
jl_serialize_value(s, codeinst->inferred);
jl_serialize_value(s, codeinst->rettype_const);
jl_serialize_value(s, codeinst->rettype);
jl_serialize_value(s, codeinst->argescapes);
}
else {
// skip storing useless data
jl_serialize_value(s, NULL);
jl_serialize_value(s, NULL);
jl_serialize_value(s, jl_any_type);
jl_serialize_value(s, jl_nothing);
}
write_uint8(s->s, codeinst->relocatability);
jl_serialize_code_instance(s, codeinst->next, skip_partial_opaque);
@@ -1667,6 +1669,8 @@ static jl_value_t *jl_deserialize_value_code_instance(jl_serializer_state *s, jl
jl_gc_wb(codeinst, codeinst->rettype_const);
codeinst->rettype = jl_deserialize_value(s, &codeinst->rettype);
jl_gc_wb(codeinst, codeinst->rettype);
codeinst->argescapes = jl_deserialize_value(s, &codeinst->argescapes);
jl_gc_wb(codeinst, codeinst->argescapes);
if (constret)
codeinst->invoke = jl_fptr_const_return;
if ((flags >> 3) & 1)
19 changes: 11 additions & 8 deletions src/gf.c
Original file line number Diff line number Diff line change
@@ -207,7 +207,8 @@ JL_DLLEXPORT jl_code_instance_t* jl_new_codeinst(
jl_method_instance_t *mi, jl_value_t *rettype,
jl_value_t *inferred_const, jl_value_t *inferred,
int32_t const_flags, size_t min_world, size_t max_world,
uint8_t ipo_effects, uint8_t effects, uint8_t relocatability);
uint8_t ipo_effects, uint8_t effects, jl_value_t *argescapes,
uint8_t relocatability);
JL_DLLEXPORT void jl_mi_cache_insert(jl_method_instance_t *mi JL_ROOTING_ARGUMENT,
jl_code_instance_t *ci JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED);

@@ -244,7 +245,7 @@ jl_datatype_t *jl_mk_builtin_func(jl_datatype_t *dt, const char *name, jl_fptr_a

jl_code_instance_t *codeinst = jl_new_codeinst(mi,
(jl_value_t*)jl_any_type, jl_nothing, jl_nothing,
0, 1, ~(size_t)0, 0, 0, 0);
0, 1, ~(size_t)0, 0, 0, jl_nothing, 0);
jl_mi_cache_insert(mi, codeinst);
codeinst->specptr.fptr1 = fptr;
codeinst->invoke = jl_fptr_args;
@@ -367,7 +368,7 @@ JL_DLLEXPORT jl_code_instance_t *jl_get_method_inferred(
}
codeinst = jl_new_codeinst(
mi, rettype, NULL, NULL,
0, min_world, max_world, 0, 0, 0);
0, min_world, max_world, 0, 0, jl_nothing, 0);
jl_mi_cache_insert(mi, codeinst);
return codeinst;
}
@@ -376,7 +377,8 @@ JL_DLLEXPORT jl_code_instance_t *jl_new_codeinst(
jl_method_instance_t *mi, jl_value_t *rettype,
jl_value_t *inferred_const, jl_value_t *inferred,
int32_t const_flags, size_t min_world, size_t max_world,
uint8_t ipo_effects, uint8_t effects, uint8_t relocatability
uint8_t ipo_effects, uint8_t effects, jl_value_t *argescapes,
uint8_t relocatability
/*, jl_array_t *edges, int absolute_max*/)
{
jl_task_t *ct = jl_current_task;
@@ -401,9 +403,10 @@ JL_DLLEXPORT jl_code_instance_t *jl_new_codeinst(
codeinst->isspecsig = 0;
codeinst->precompile = 0;
codeinst->next = NULL;
codeinst->relocatability = relocatability;
codeinst->ipo_purity_bits = ipo_effects;
codeinst->purity_bits = effects;
codeinst->argescapes = argescapes;
codeinst->relocatability = relocatability;
return codeinst;
}

@@ -2013,7 +2016,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t
if (unspec && jl_atomic_load_relaxed(&unspec->invoke)) {
jl_code_instance_t *codeinst = jl_new_codeinst(mi,
(jl_value_t*)jl_any_type, NULL, NULL,
0, 1, ~(size_t)0, 0, 0, 0);
0, 1, ~(size_t)0, 0, 0, jl_nothing, 0);
codeinst->isspecsig = 0;
codeinst->specptr = unspec->specptr;
codeinst->rettype_const = unspec->rettype_const;
@@ -2031,7 +2034,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t
if (!jl_code_requires_compiler(src)) {
jl_code_instance_t *codeinst = jl_new_codeinst(mi,
(jl_value_t*)jl_any_type, NULL, NULL,
0, 1, ~(size_t)0, 0, 0, 0);
0, 1, ~(size_t)0, 0, 0, jl_nothing, 0);
codeinst->invoke = jl_fptr_interpret_call;
jl_mi_cache_insert(mi, codeinst);
record_precompile_statement(mi);
@@ -2066,7 +2069,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t
return ucache;
}
codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, NULL, NULL,
0, 1, ~(size_t)0, 0, 0, 0);
0, 1, ~(size_t)0, 0, 0, jl_nothing, 0);
codeinst->isspecsig = 0;
codeinst->specptr = ucache->specptr;
codeinst->rettype_const = ucache->rettype_const;
12 changes: 7 additions & 5 deletions src/jltypes.c
Original file line number Diff line number Diff line change
@@ -2492,7 +2492,7 @@ void jl_init_types(void) JL_GC_DISABLED
jl_code_instance_type =
jl_new_datatype(jl_symbol("CodeInstance"), core,
jl_any_type, jl_emptysvec,
jl_perm_symsvec(14,
jl_perm_symsvec(15,
"def",
"next",
"min_world",
@@ -2502,10 +2502,11 @@ void jl_init_types(void) JL_GC_DISABLED
"inferred",
//"edges",
//"absolute_max",
"ipo_purity_bits", "purity_bits",
"ipo_purity_bits", "purity_bits",
"argescapes",
"isspecsig", "precompile", "invoke", "specptr", // function object decls
"relocatability"),
jl_svec(14,
jl_svec(15,
jl_method_instance_type,
jl_any_type,
jl_ulong_type,
@@ -2515,7 +2516,8 @@ void jl_init_types(void) JL_GC_DISABLED
jl_any_type,
//jl_any_type,
//jl_bool_type,
jl_uint8_type, jl_uint8_type,
jl_uint8_type, jl_uint8_type,
jl_any_type,
jl_bool_type,
jl_bool_type,
jl_any_type, jl_any_type, // fptrs
@@ -2668,8 +2670,8 @@ void jl_init_types(void) JL_GC_DISABLED
jl_svecset(jl_methtable_type->types, 11, jl_uint8_type);
jl_svecset(jl_method_type->types, 12, jl_method_instance_type);
jl_svecset(jl_method_instance_type->types, 6, jl_code_instance_type);
jl_svecset(jl_code_instance_type->types, 11, jl_voidpointer_type);
jl_svecset(jl_code_instance_type->types, 12, jl_voidpointer_type);
jl_svecset(jl_code_instance_type->types, 13, jl_voidpointer_type);

jl_compute_field_offsets(jl_datatype_type);
jl_compute_field_offsets(jl_typename_type);
1 change: 1 addition & 0 deletions src/julia.h
Original file line number Diff line number Diff line change
@@ -409,6 +409,7 @@ typedef struct _jl_code_instance_t {
uint8_t terminates:2;
} purity_flags;
};
jl_value_t *argescapes; // escape information of call arguments

// compilation state cache
uint8_t isspecsig; // if specptr is a specialized function signature for specTypes->rettype
5 changes: 4 additions & 1 deletion test/choosetests.jl
Original file line number Diff line number Diff line change
@@ -142,7 +142,10 @@ function choosetests(choices = [])
filtertests!(tests, "subarray")
filtertests!(tests, "compiler", ["compiler/inference", "compiler/validation",
"compiler/ssair", "compiler/irpasses", "compiler/codegen",
"compiler/inline", "compiler/contextual"])
"compiler/inline", "compiler/contextual",
"compiler/EscapeAnalysis/local", "compiler/EscapeAnalysis/interprocedural"])
filtertests!(tests, "compiler/EscapeAnalysis", [
"compiler/EscapeAnalysis/local", "compiler/EscapeAnalysis/interprocedural"])
filtertests!(tests, "stdlib", STDLIBS)
# do ambiguous first to avoid failing if ambiguities are introduced by other tests
filtertests!(tests, "ambiguous")
385 changes: 385 additions & 0 deletions test/compiler/EscapeAnalysis/EAUtils.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,385 @@
module EAUtils

export code_escapes, @code_escapes, __clear_cache!

const CC = Core.Compiler
const EA = CC.EscapeAnalysis

# entries
# -------

import Base: unwrap_unionall, rewrap_unionall
import InteractiveUtils: gen_call_with_extracted_types_and_kwargs

"""
@code_escapes [options...] f(args...)
Evaluates the arguments to the function call, determines its types, and then calls
[`code_escapes`](@ref) on the resulting expression.
As with `@code_typed` and its family, any of `code_escapes` keyword arguments can be given
as the optional arguments like `@code_escapes optimize=false myfunc(myargs...)`.
"""
macro code_escapes(ex0...)
return gen_call_with_extracted_types_and_kwargs(__module__, :code_escapes, ex0)
end

"""
code_escapes(f, argtypes=Tuple{}; [debuginfo::Symbol = :none], [optimize::Bool = true]) -> result::EscapeResult
Runs the escape analysis on optimized IR of a generic function call with the given type signature.
# Keyword Arguments
- `optimize::Bool = true`:
if `true` returns escape information of post-inlining IR (used for local optimization),
otherwise returns escape information of pre-inlining IR (used for interprocedural escape information generation)
- `debuginfo::Symbol = :none`:
controls the amount of code metadata present in the output, possible options are `:none` or `:source`.
"""
function code_escapes(@nospecialize(f), @nospecialize(types=Base.default_tt(f));
world::UInt = get_world_counter(),
interp::Core.Compiler.AbstractInterpreter = Core.Compiler.NativeInterpreter(world),
debuginfo::Symbol = :none,
optimize::Bool = true)
ft = Core.Typeof(f)
if isa(types, Type)
u = unwrap_unionall(types)
tt = rewrap_unionall(Tuple{ft, u.parameters...}, types)
else
tt = Tuple{ft, types...}
end
interp = EscapeAnalyzer(interp, tt, optimize)
results = Base.code_typed_by_type(tt; optimize=true, world, interp)
isone(length(results)) || throw(ArgumentError("`code_escapes` only supports single analysis result"))
return EscapeResult(interp.ir, interp.state, interp.linfo, debuginfo===:source)
end

# in order to run a whole analysis from ground zero (e.g. for benchmarking, etc.)
__clear_cache!() = empty!(GLOBAL_CODE_CACHE)

# AbstractInterpreter
# -------------------

# imports
import .CC:
AbstractInterpreter, NativeInterpreter, WorldView, WorldRange,
InferenceParams, OptimizationParams, get_world_counter, get_inference_cache, code_cache,
lock_mi_inference, unlock_mi_inference, add_remark!,
may_optimize, may_compress, may_discard_trees, verbose_stmt_info
# usings
import Core:
CodeInstance, MethodInstance, CodeInfo
import .CC:
InferenceResult, OptimizationState, IRCode, copy as cccopy,
@timeit, convert_to_ircode, slot2reg, compact!, ssa_inlining_pass!, sroa_pass!,
adce_pass!, type_lift_pass!, JLOptions, verify_ir, verify_linetable
import .EA: analyze_escapes, ArgEscapeCache, EscapeInfo, EscapeState, is_ipo_profitable

# when working outside of Core.Compiler,
# cache entire escape state for later inspection and debugging
struct EscapeCache
cache::ArgEscapeCache
state::EscapeState # preserved just for debugging purpose
ir::IRCode # preserved just for debugging purpose
end

mutable struct EscapeAnalyzer{State} <: AbstractInterpreter
native::NativeInterpreter
cache::IdDict{InferenceResult,EscapeCache}
entry_tt
optimize::Bool
ir::IRCode
state::State
linfo::MethodInstance
EscapeAnalyzer(native::NativeInterpreter, @nospecialize(tt), optimize::Bool) =
new{EscapeState}(native, IdDict{InferenceResult,EscapeCache}(), tt, optimize)
end

CC.InferenceParams(interp::EscapeAnalyzer) = InferenceParams(interp.native)
CC.OptimizationParams(interp::EscapeAnalyzer) = OptimizationParams(interp.native)
CC.get_world_counter(interp::EscapeAnalyzer) = get_world_counter(interp.native)

CC.lock_mi_inference(::EscapeAnalyzer, ::MethodInstance) = nothing
CC.unlock_mi_inference(::EscapeAnalyzer, ::MethodInstance) = nothing

CC.add_remark!(interp::EscapeAnalyzer, sv, s) = add_remark!(interp.native, sv, s)

CC.may_optimize(interp::EscapeAnalyzer) = may_optimize(interp.native)
CC.may_compress(interp::EscapeAnalyzer) = may_compress(interp.native)
CC.may_discard_trees(interp::EscapeAnalyzer) = may_discard_trees(interp.native)
CC.verbose_stmt_info(interp::EscapeAnalyzer) = verbose_stmt_info(interp.native)

CC.get_inference_cache(interp::EscapeAnalyzer) = get_inference_cache(interp.native)

const GLOBAL_CODE_CACHE = IdDict{MethodInstance,CodeInstance}()

function CC.code_cache(interp::EscapeAnalyzer)
worlds = WorldRange(get_world_counter(interp))
return WorldView(GlobalCache(), worlds)
end

struct GlobalCache end

CC.haskey(wvc::WorldView{GlobalCache}, mi::MethodInstance) = haskey(GLOBAL_CODE_CACHE, mi)

CC.get(wvc::WorldView{GlobalCache}, mi::MethodInstance, default) = get(GLOBAL_CODE_CACHE, mi, default)

CC.getindex(wvc::WorldView{GlobalCache}, mi::MethodInstance) = getindex(GLOBAL_CODE_CACHE, mi)

function CC.setindex!(wvc::WorldView{GlobalCache}, ci::CodeInstance, mi::MethodInstance)
GLOBAL_CODE_CACHE[mi] = ci
add_callback!(mi) # register the callback on invalidation
return nothing
end

function add_callback!(linfo)
if !isdefined(linfo, :callbacks)
linfo.callbacks = Any[invalidate_cache!]
else
if !any(@nospecialize(cb)->cb===invalidate_cache!, linfo.callbacks)
push!(linfo.callbacks, invalidate_cache!)
end
end
return nothing
end

function invalidate_cache!(replaced, max_world, depth = 0)
delete!(GLOBAL_CODE_CACHE, replaced)

if isdefined(replaced, :backedges)
for mi in replaced.backedges
mi = mi::MethodInstance
if !haskey(GLOBAL_CODE_CACHE, mi)
continue # otherwise fall into infinite loop
end
invalidate_cache!(mi, max_world, depth+1)
end
end
return nothing
end

function CC.optimize(interp::EscapeAnalyzer,
opt::OptimizationState, params::OptimizationParams, caller::InferenceResult)
ir = run_passes_with_ea(interp, opt.src, opt, caller)
return CC.finish(interp, opt, params, ir, caller)
end

function CC.cache_result!(interp::EscapeAnalyzer, caller::InferenceResult)
if haskey(interp.cache, caller)
GLOBAL_ESCAPE_CACHE[caller.linfo] = interp.cache[caller]
end
return Base.@invoke CC.cache_result!(interp::AbstractInterpreter, caller::InferenceResult)
end

const GLOBAL_ESCAPE_CACHE = IdDict{MethodInstance,EscapeCache}()

"""
cache_escapes!(caller::InferenceResult, estate::EscapeState, cacheir::IRCode)
Transforms escape information of call arguments of `caller`,
and then caches it into a global cache for later interprocedural propagation.
"""
function cache_escapes!(interp::EscapeAnalyzer,
caller::InferenceResult, estate::EscapeState, cacheir::IRCode)
cache = ArgEscapeCache(estate)
ecache = EscapeCache(cache, estate, cacheir)
interp.cache[caller] = ecache
return cache
end

function get_escape_cache(interp::EscapeAnalyzer)
return function (linfo::Union{InferenceResult,MethodInstance})
if isa(linfo, InferenceResult)
ecache = get(interp.cache, linfo, nothing)
else
ecache = get(GLOBAL_ESCAPE_CACHE, linfo, nothing)
end
return ecache !== nothing ? ecache.cache : nothing
end
end

function run_passes_with_ea(interp::EscapeAnalyzer, ci::CodeInfo, sv::OptimizationState,
caller::InferenceResult)
@timeit "convert" ir = convert_to_ircode(ci, sv)
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)
# TODO: Domsorting can produce an updated domtree - no need to recompute here
@timeit "compact 1" ir = compact!(ir)
nargs = let def = sv.linfo.def; isa(def, Method) ? Int(def.nargs) : 0; end
local state
if is_ipo_profitable(ir, nargs) || caller.linfo.specTypes === interp.entry_tt
try
@timeit "[IPO EA]" begin
state = analyze_escapes(ir, nargs, false, get_escape_cache(interp))
cache_escapes!(interp, caller, state, cccopy(ir))
end
catch err
@error "error happened within [IPO EA], insepct `Main.ir` and `Main.nargs`"
@eval Main (ir = $ir; nargs = $nargs)
rethrow(err)
end
end
if caller.linfo.specTypes === interp.entry_tt && !interp.optimize
# return back the result
interp.ir = cccopy(ir)
interp.state = state
interp.linfo = sv.linfo
end
@timeit "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds)
# @timeit "verify 2" verify_ir(ir)
@timeit "compact 2" ir = compact!(ir)
if caller.linfo.specTypes === interp.entry_tt && interp.optimize
try
@timeit "[Local EA]" state = analyze_escapes(ir, nargs, true, get_escape_cache(interp))
catch err
@error "error happened within [Local EA], insepct `Main.ir` and `Main.nargs`"
@eval Main (ir = $ir; nargs = $nargs)
rethrow(err)
end
# return back the result
interp.ir = cccopy(ir)
interp.state = state
interp.linfo = sv.linfo
end
@timeit "SROA" ir = sroa_pass!(ir)
@timeit "ADCE" ir = adce_pass!(ir)
@timeit "type lift" ir = type_lift_pass!(ir)
@timeit "compact 3" ir = compact!(ir)
if JLOptions().debug_level == 2
@timeit "verify 3" (verify_ir(ir); verify_linetable(ir.linetable))
end
return ir
end

# printing
# --------

import Core: Argument, SSAValue
import .CC: widenconst, singleton_type

Base.getindex(estate::EscapeState, @nospecialize(x)) = CC.getindex(estate, x)

function get_name_color(x::EscapeInfo, symbol::Bool = false)
getname(x) = string(nameof(x))
if x === EA.⊥
name, color = (getname(EA.NotAnalyzed), ""), :plain
elseif EA.has_no_escape(EA.ignore_argescape(x))
if EA.has_arg_escape(x)
name, color = (getname(EA.ArgEscape), ""), :cyan
else
name, color = (getname(EA.NoEscape), ""), :green
end
elseif EA.has_all_escape(x)
name, color = (getname(EA.AllEscape), "X"), :red
elseif EA.has_return_escape(x)
name = (getname(EA.ReturnEscape), "")
color = EA.has_thrown_escape(x) ? :yellow : :blue
else
name = (nothing, "*")
color = EA.has_thrown_escape(x) ? :yellow : :bold
end
name = symbol ? last(name) : first(name)
if name !== nothing && !isa(x.AliasInfo, Bool)
name = string(name, "")
end
return name, color
end

# pcs = sprint(show, collect(x.EscapeSites); context=:limit=>true)
function Base.show(io::IO, x::EscapeInfo)
name, color = get_name_color(x)
if isnothing(name)
Base.@invoke show(io::IO, x::Any)
else
printstyled(io, name; color)
end
end
function Base.show(io::IO, ::MIME"application/prs.juno.inline", x::EscapeInfo)
name, color = get_name_color(x)
if isnothing(name)
return x # use fancy tree-view
else
printstyled(io, name; color)
end
end

struct EscapeResult
ir::IRCode
state::EscapeState
linfo::Union{Nothing,MethodInstance}
source::Bool
function EscapeResult(ir::IRCode, state::EscapeState,
linfo::Union{Nothing,MethodInstance} = nothing,
source::Bool=false)
return new(ir, state, linfo, source)
end
end
Base.show(io::IO, result::EscapeResult) = print_with_info(io, result)
@eval Base.iterate(res::EscapeResult, state=1) =
return state > $(fieldcount(EscapeResult)) ? nothing : (getfield(res, state), state+1)

Base.show(io::IO, cached::EscapeCache) = show(io, EscapeResult(cached.ir, cached.state, nothing))

# adapted from https://github.com/JuliaDebug/LoweredCodeUtils.jl/blob/4612349432447e868cf9285f647108f43bd0a11c/src/codeedges.jl#L881-L897
function print_with_info(io::IO, (; ir, state, linfo, source)::EscapeResult)
# print escape information on SSA values
function preprint(io::IO)
ft = ir.argtypes[1]
f = singleton_type(ft)
if f === nothing
f = widenconst(ft)
end
print(io, f, '(')
for i in 1:state.nargs
arg = state[Argument(i)]
i == 1 && continue
c, color = get_name_color(arg, true)
printstyled(io, c, ' ', '_', i, "::", ir.argtypes[i]; color)
i state.nargs && print(io, ", ")
end
print(io, ')')
if !isnothing(linfo)
def = linfo.def
printstyled(io, " in ", (isa(def, Module) ? (def,) : (def.module, " at ", def.file, ':', def.line))...; color=:bold)
end
println(io)
end

# print escape information on SSA values
# nd = ndigits(length(ssavalues))
function preprint(io::IO, idx::Int)
c, color = get_name_color(state[SSAValue(idx)], true)
# printstyled(io, lpad(idx, nd), ' ', c, ' '; color)
printstyled(io, rpad(c, 2), ' '; color)
end

print_with_info(preprint, (args...)->nothing, io, ir, source)
end

function print_with_info(preprint, postprint, io::IO, ir::IRCode, source::Bool)
io = IOContext(io, :displaysize=>displaysize(io))
used = Base.IRShow.stmts_used(io, ir)
if source
line_info_preprinter = function (io::IO, indent::String, idx::Int)
r = Base.IRShow.inline_linfo_printer(ir)(io, indent, idx)
idx 0 && preprint(io, idx)
return r
end
else
line_info_preprinter = Base.IRShow.lineinfo_disabled
end
line_info_postprinter = Base.IRShow.default_expr_type_printer
preprint(io)
bb_idx_prev = bb_idx = 1
for idx = 1:length(ir.stmts)
preprint(io, idx)
bb_idx = Base.IRShow.show_ir_stmt(io, ir, idx, line_info_preprinter, line_info_postprinter, used, ir.cfg, bb_idx)
postprint(io, idx, bb_idx != bb_idx_prev)
bb_idx_prev = bb_idx
end
max_bb_idx_size = ndigits(length(ir.cfg.blocks))
line_info_preprinter(io, " "^(max_bb_idx_size + 2), 0)
postprint(io)
return nothing
end

end # module EAUtils
262 changes: 262 additions & 0 deletions test/compiler/EscapeAnalysis/interprocedural.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# IPO EA Test
# ===========
# EA works on pre-inlining IR

include(normpath(@__DIR__, "setup.jl"))

# callsites
# ---------

noescape(a) = nothing
noescape(a, b) = nothing
function global_escape!(x)
GR[] = x
return nothing
end
union_escape!(x) = global_escape!(x)
union_escape!(x::SafeRef) = nothing
union_escape!(x::SafeRefs) = nothing
Base.@constprop :aggressive function conditional_escape!(cnd, x)
cnd && global_escape!(x)
return nothing
end

# MethodMatchInfo -- global cache
let result = code_escapes((SafeRef{String},); optimize=false) do x
return noescape(x)
end
@test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
identity(x)
return nothing
end
@test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
return identity(x)
end
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
return Ref(x)
end
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
r = Ref{SafeRef{String}}()
r[] = x
return r
end
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
global_escape!(x)
end
@test has_all_escape(result.state[Argument(2)])
end
# UnionSplitInfo
let result = code_escapes((Bool,Vector{Any}); optimize=false) do c, s
x = c ? s : SafeRef(s)
union_escape!(x)
end
@test has_all_escape(result.state[Argument(3)]) # s
end
let result = code_escapes((Bool,Vector{Any}); optimize=false) do c, s
x = c ? SafeRef(s) : SafeRefs(s, s)
union_escape!(x)
end
@test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
# ConstCallInfo -- local cache
let result = code_escapes((SafeRef{String},); optimize=false) do x
return conditional_escape!(false, x)
end
@test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
# InvokeCallInfo
let result = code_escapes((SafeRef{String},); optimize=false) do x
return Base.@invoke noescape(x::Any)
end
@test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
return Base.@invoke conditional_escape!(false::Any, x::Any)
end
@test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end

# MethodError
# -----------
# accounts for ThrownEscape via potential MethodError

# no method error
identity_if_string(x::SafeRef) = nothing
let result = code_escapes((SafeRef{String},); optimize=false) do x
identity_if_string(x)
end
i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test !has_thrown_escape(result.state[Argument(2)], i)
@test !has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((Union{SafeRef{String},Vector{String}},); optimize=false) do x
identity_if_string(x)
end
i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_thrown_escape(result.state[Argument(2)], i)
@test !has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((SafeRef{String},); optimize=false) do x
try
identity_if_string(x)
catch err
global GV = err
end
return nothing
end
@test !has_all_escape(result.state[Argument(2)])
end
let result = code_escapes((Union{SafeRef{String},Vector{String}},); optimize=false) do x
try
identity_if_string(x)
catch err
global GV = err
end
return nothing
end
@test has_all_escape(result.state[Argument(2)])
end
# method ambiguity error
ambig_error_test(a::SafeRef, b) = nothing
ambig_error_test(a, b::SafeRef) = nothing
ambig_error_test(a, b) = nothing
let result = code_escapes((SafeRef{String},Any); optimize=false) do x, y
ambig_error_test(x, y)
end
i = only(findall(iscall((result.ir, ambig_error_test)), result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_thrown_escape(result.state[Argument(2)], i) # x
@test has_thrown_escape(result.state[Argument(3)], i) # y
@test !has_return_escape(result.state[Argument(2)], r) # x
@test !has_return_escape(result.state[Argument(3)], r) # y
end
let result = code_escapes((SafeRef{String},Any); optimize=false) do x, y
try
ambig_error_test(x, y)
catch err
global GV = err
end
end
@test has_all_escape(result.state[Argument(2)]) # x
@test has_all_escape(result.state[Argument(3)]) # y
end

# Local EA integration
# --------------------

# propagate escapes imposed on call arguments

# FIXME handle _apply_iterate
# FIXME currently we can't prove the effect-freeness of `getfield(RefValue{String}, :x)`
# because of this check https://github.com/JuliaLang/julia/blob/94b9d66b10e8e3ebdb268e4be5f7e1f43079ad4e/base/compiler/tfuncs.jl#L745
# and thus it leads to the following two broken tests

@noinline broadcast_noescape1(a) = (broadcast(identity, a); nothing)
let result = code_escapes() do
broadcast_noescape1(Ref("Hi"))
end
i = only(findall(isnew, result.ir.stmts.inst))
@test_broken !has_return_escape(result.state[SSAValue(i)])
@test_broken !has_thrown_escape(result.state[SSAValue(i)])
end
@noinline broadcast_noescape2(b) = broadcast(identity, b)
let result = code_escapes() do
broadcast_noescape2(Ref("Hi"))
end
i = only(findall(isnew, result.ir.stmts.inst))
@test_broken !has_return_escape(result.state[SSAValue(i)])
@test_broken !has_thrown_escape(result.state[SSAValue(i)])
end
@noinline allescape_argument(a) = (global GV = a) # obvious escape
let result = code_escapes() do
allescape_argument(Ref("Hi"))
end
i = only(findall(isnew, result.ir.stmts.inst))
@test has_all_escape(result.state[SSAValue(i)])
end
# if we can't determine the matching method statically, we should be conservative
let result = code_escapes((Ref{Any},)) do a
may_exist(a)
end
@test has_all_escape(result.state[Argument(2)])
end
let result = code_escapes((Ref{Any},)) do a
Base.@invokelatest broadcast_noescape1(a)
end
@test has_all_escape(result.state[Argument(2)])
end

# handling of simple union-split (just exploit the inliner's effort)
@noinline unionsplit_noescape(a) = string(nothing)
@noinline unionsplit_noescape(a::Int) = a + 10
let result = code_escapes((Union{Int,Nothing},)) do x
s = SafeRef{Union{Int,Nothing}}(x)
unionsplit_noescape(s[])
return nothing
end
inds = findall(isnew, result.ir.stmts.inst) # find allocation statement
@assert !isempty(inds)
for i in inds
@test has_no_escape(result.state[SSAValue(i)])
end
end

@noinline function unused_argument(a)
println("prevent inlining")
return Base.inferencebarrier(nothing)
end
let result = code_escapes() do
a = Ref("foo") # shouldn't be "return escape"
b = unused_argument(a)
nothing
end
i = only(findall(isnew, result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test !has_return_escape(result.state[SSAValue(i)], r)

result = code_escapes() do
a = Ref("foo") # still should be "return escape"
b = unused_argument(a)
return a
end
i = only(findall(isnew, result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_return_escape(result.state[SSAValue(i)], r)
end

# should propagate escape information imposed on return value to the aliased call argument
@noinline returnescape_argument(a) = (println("prevent inlining"); a)
let result = code_escapes() do
obj = Ref("foo") # should be "return escape"
ret = returnescape_argument(obj)
return ret # alias of `obj`
end
i = only(findall(isnew, result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test has_return_escape(result.state[SSAValue(i)], r)
end
@noinline noreturnescape_argument(a) = (println("prevent inlining"); identity("hi"))
let result = code_escapes() do
obj = Ref("foo") # better to not be "return escape"
ret = noreturnescape_argument(obj)
return ret # must not alias to `obj`
end
i = only(findall(isnew, result.ir.stmts.inst))
r = only(findall(isreturn, result.ir.stmts.inst))
@test !has_return_escape(result.state[SSAValue(i)], r)
end
2,206 changes: 2,206 additions & 0 deletions test/compiler/EscapeAnalysis/local.jl

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions test/compiler/EscapeAnalysis/setup.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
include(normpath(@__DIR__, "EAUtils.jl"))
using Test, Core.Compiler.EscapeAnalysis, .EAUtils
import Core: Argument, SSAValue, ReturnNode
const EA = Core.Compiler.EscapeAnalysis
import .EA: ignore_argescape

isT(T) = (@nospecialize x) -> x === T
isreturn(@nospecialize x) = isa(x, Core.ReturnNode) && isdefined(x, :val)
isthrow(@nospecialize x) = Meta.isexpr(x, :call) && Core.Compiler.is_throw_call(x)
isnew(@nospecialize x) = Meta.isexpr(x, :new)
isϕ(@nospecialize x) = isa(x, Core.PhiNode)
function with_normalized_name(@nospecialize(f), @nospecialize(x))
if Meta.isexpr(x, :foreigncall)
name = x.args[1]
nn = EA.normalize(name)
return isa(nn, Symbol) && f(nn)
end
return false
end
isarrayalloc(@nospecialize x) = with_normalized_name(nn->!isnothing(Core.Compiler.alloc_array_ndims(nn)), x)
isarrayresize(@nospecialize x) = with_normalized_name(nn->!isnothing(EA.array_resize_info(nn)), x)
isarraycopy(@nospecialize x) = with_normalized_name(nn->EA.is_array_copy(nn), x)
import Core.Compiler: argextype, singleton_type
iscall(y) = @nospecialize(x) -> iscall(y, x)
function iscall((ir, f), @nospecialize(x))
return iscall(x) do @nospecialize x
singleton_type(Core.Compiler.argextype(x, ir, Any[])) === f
end
end
iscall(pred::Function, @nospecialize(x)) = Meta.isexpr(x, :call) && pred(x.args[1])

# check if `x` is a statically-resolved call of a function whose name is `sym`
isinvoke(y) = @nospecialize(x) -> isinvoke(y, x)
isinvoke(sym::Symbol, @nospecialize(x)) = isinvoke(mi->mi.def.name===sym, x)
isinvoke(pred::Function, @nospecialize(x)) = Meta.isexpr(x, :invoke) && pred(x.args[1]::Core.MethodInstance)

"""
is_load_forwardable(x::EscapeInfo) -> Bool
Queries if `x` is elibigle for store-to-load forwarding optimization.
"""
function is_load_forwardable(x::EA.EscapeInfo)
AliasInfo = x.AliasInfo
# NOTE technically we also need to check `!has_thrown_escape(x)` here as well,
# but we can also do equivalent check during forwarding
return isa(AliasInfo, EA.IndexableFields) || isa(AliasInfo, EA.IndexableElements)
end

let setup_ex = quote
mutable struct SafeRef{T}
x::T
end
Base.getindex(s::SafeRef) = getfield(s, 1)
Base.setindex!(s::SafeRef, x) = setfield!(s, 1, x)

mutable struct SafeRefs{S,T}
x1::S
x2::T
end
Base.getindex(s::SafeRefs, idx::Int) = getfield(s, idx)
Base.setindex!(s::SafeRefs, x, idx::Int) = setfield!(s, idx, x)

global GV::Any
const global GR = Ref{Any}()
end
global function EATModule(setup_ex = setup_ex)
M = Module()
Core.eval(M, setup_ex)
return M
end
Core.eval(@__MODULE__, setup_ex)
end