- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
+5,690
−96
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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
1,913
base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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