EscapeAnalysis
Core.Compiler.EscapeAnalysis
— это модуль утилиты компилятора, который предназначен для анализа информации о выходе Julia's SSA-form IR, также известного как IRCode
.
Этот анализ побега направлен на:
- использовать высокоуровневую семантику Julia, особенно рассматривать побеги и алиасинг через межпроцедурные вызовы
- быть достаточно универсальным, чтобы использоваться для различных оптимизаций, включая alias-aware SROA, early
finalize
insertion, copy-freeImmutableArray
construction, стековое выделение изменяемых объектов и так далее. - достигнуть простой реализации на основе полностью обратного анализа данных, а также нового проектирования решётки, которое сочетает в себе ортогональные свойства решётки
Try it out!
Вы можете попробовать анализ утечек, загрузив утилитный скрипт EAUtils.jl
, который определяет удобные записи code_escapes
и @code_escapes
для тестирования и отладки:
julia> let JULIA_DIR = normpath(Sys.BINDIR, "..", "share", "julia") # load `EscapeAnalysis` module to define the core analysis code include(normpath(JULIA_DIR, "base", "compiler", "ssair", "EscapeAnalysis", "EscapeAnalysis.jl")) using .EscapeAnalysis # load `EAUtils` module to define the utilities include(normpath(JULIA_DIR, "test", "compiler", "EscapeAnalysis", "EAUtils.jl")) using .EAUtils end
julia> mutable struct SafeRef{T} x::T end
julia> Base.getindex(x::SafeRef) = x.x;
julia> Base.setindex!(x::SafeRef, v) = x.x = v;
julia> Base.isassigned(x::SafeRef) = true;
julia> get′(x) = isassigned(x) ? x[] : throw(x);
julia> result = code_escapes((String,String,String,String)) do s1, s2, s3, s4 r1 = Ref(s1) r2 = Ref(s2) r3 = SafeRef(s3) try s1 = get′(r1) ret = sizeof(s1) catch err global GV = err # will definitely escape `r1` end s2 = get′(r2) # still `r2` doesn't escape fully s3 = get′(r3) # still `r3` doesn't escape fully s4 = sizeof(s4) # the argument `s4` doesn't escape here return s2, s3, s4 end
#1(X s1::String, ↑ s2::String, ↑ s3::String, ✓ s4::String) in Main at REPL[7]:2 X 1 ── %1 = %new(Base.RefValue{String}, _2)::Base.RefValue{String} *′ │ %2 = %new(Base.RefValue{String}, _3)::Base.RefValue{String} ✓′ └─── %3 = %new(Main.SafeRef{String}, _4)::Main.SafeRef{String} ✓′ 2 ── %4 = ϒ (%3)::Main.SafeRef{String} *′ │ %5 = ϒ (%2)::Base.RefValue{String} ✓ │ %6 = ϒ (_5)::String ◌ └─── %7 = enter #8 ◌ 3 ── %8 = Base.isdefined(%1, :x)::Bool ◌ └─── goto #5 if not %8 X 4 ── Base.getfield(%1, :x)::String ◌ └─── goto #6 ◌ 5 ── Main.throw(%1)::Union{} ◌ └─── unreachable ◌ 6 ── $(Expr(:leave, :(%7))) ◌ 7 ── goto #11 ✓′ 8 ┄─ %16 = φᶜ (%4)::Main.SafeRef{String} *′ │ %17 = φᶜ (%5)::Base.RefValue{String} ✓ │ %18 = φᶜ (%6)::String X └─── %19 = $(Expr(:the_exception))::Any ◌ 9 ── nothing::Nothing ◌ 10 ─ (Main.GV = %19)::Any ◌ └─── $(Expr(:pop_exception, :(%7)))::Core.Const(nothing) ✓′ 11 ┄ %23 = φ (#7 => %3, #10 => %16)::Main.SafeRef{String} *′ │ %24 = φ (#7 => %2, #10 => %17)::Base.RefValue{String} ✓ │ %25 = φ (#7 => _5, #10 => %18)::String ◌ │ %26 = Base.isdefined(%24, :x)::Bool ◌ └─── goto #13 if not %26 ↑ 12 ─ %28 = Base.getfield(%24, :x)::String ◌ └─── goto #14 ◌ 13 ─ Main.throw(%24)::Union{} ◌ └─── unreachable ↑ 14 ─ %32 = Base.getfield(%23, :x)::String ◌ │ %33 = Core.sizeof(%25)::Int64 ↑′ │ %34 = Core.tuple(%28, %32, %33)::Tuple{String, String, Int64} ◌ └─── return %34
Символы сбоку от каждого аргумента вызова и операторов SSA представляют следующее значение:
◌
(plain): это значение не анализируется, потому что информация об экранировании в любом случае не будет использована (например, когда объект являетсяisbitstype
)✓
(зеленый или циановый): это значение никогда не уходит (has_no_escape(result.state[x])
верно), окрашено в синий, если также есть аргумент уходит (has_arg_escape(result.state[x])
верно)↑
(синий или желтый): это значение может выйти к вызывающему через возврат (has_return_escape(result.state[x])
истинно), окрашено в желтый, если также есть необработанное выброшенное значение (has_thrown_escape(result.state[x])
истинно)X
(красный): это значение может уйти куда-то, о чем анализ побегов не может рассуждать, например, уйти в глобальную память (has_all_escape(result.state[x])
верно)*
(жирный): состояние экранирования этого значения находится междуReturnEscape
иAllEscape
в частичном порядкеEscapeInfo
, окрашено в желтый, если оно имеет необработанное выброшенное экранирование также (has_thrown_escape(result.state[x])
истинно)′
: это значение имеет дополнительную информацию о поле объекта / элементе массива в его свойствеAliasInfo
Информацию о каждом аргументе вызова и значении SSA можно просмотреть программно, как:
julia> result.state[Core.Argument(3)] # get EscapeInfo of `s2`
ReturnEscape
julia> result.state[Core.SSAValue(3)] # get EscapeInfo of `r3`
NoEscape′
Analysis Design
Lattice Design
EscapeAnalysis
реализован как data-flow analysis, который работает на решетке x::EscapeInfo
, которая состоит из следующих свойств:
x.Analyzed::Bool
: не является формальной частью решетки, только указывает на то, чтоx
не был проанализирован или нетx.ReturnEscape::BitSet
: записывает SSA-операторы, гдеx
может выйти к вызывающему через возвратx.ThrownEscape::BitSet
: записывает SSA-операторы, гдеx
может быть выброшен как исключение (используется для exception handling, описанного ниже)x.AliasInfo
: поддерживает все возможные значения, которые могут быть алиасированы к полям или элементам массиваx
(используется для alias analysis, описанного ниже)x.ArgEscape::Int
(пока не реализовано): указывает, что оно будет передано вызывающему черезsetfield!
на аргумент(ах)
Эти атрибуты можно комбинировать для создания частичной решетки, которая имеет конечную высоту, при условии, что входная программа имеет конечное количество операторов, что гарантируется семантикой Julia. Умная часть этого дизайна решетки заключается в том, что она позволяет более простой реализации операций решетки, позволяя им обрабатывать каждое свойство решетки отдельно[LatticeDesign].
Backward Escape Propagation
Этот вариант реализации анализа выхода основан на алгоритме потоков данных, описанном в статье[MM02]. Анализ работает на решетке EscapeInfo
и переводит элементы решетки снизу вверх, пока каждый элемент решетки не сойдется к фиксированной точке, поддерживая (концептуальный) рабочий набор, который содержит счетчики программы, соответствующие оставшимся инструкциям SSA, которые необходимо проанализировать. Анализ управляет единственным глобальным состоянием, которое отслеживает EscapeInfo
каждого аргумента и инструкции SSA, но также следует отметить, что некоторый потокочувствительный анализ закодирован в счетчиках программы, записанных в свойстве ReturnEscape
EscapeInfo
, которые могут быть объединены с анализом доминирования позже для обоснования потокочувствительности, если это необходимо.
Одним из отличительных дизайнов этого анализа выхода является то, что он полностью обратный, т.е. информация о выходе течет от использований к определениям. Например, в приведенном ниже фрагменте кода EA сначала анализирует оператор return %1
и накладывает ReturnEscape
на %1
(соответствующий obj
), а затем анализирует %1 = %new(Base.RefValue{String, _2}))
и распространяет ReturnEscape
, наложенный на %1
, на аргумент вызова _2
(соответствующий s
):
julia> code_escapes((String,)) do s obj = Ref(s) return obj end
#3(↑ s::String) in Main at REPL[1]:2 ↑′ 1 ─ %1 = %new(Base.RefValue{String}, _2)::Base.RefValue{String} ◌ └── return %1
Ключевое наблюдение здесь заключается в том, что этот обратный анализ позволяет информации о выходе естественным образом течь вдоль цепочки использования-определения, а не по потоку управления[BackandForth]. В результате эта схема позволяет просто реализовать анализ выхода, например, PhiNode
можно просто обрабатывать, распространяя информацию о выходе, наложенную на PhiNode
, на его предшествующие значения:
julia> code_escapes((Bool, String, String)) do cnd, s, t if cnd obj = Ref(s) else obj = Ref(t) end return obj end
#5(✓ cnd::Bool, ↑ s::String, ↑ t::String) in Main at REPL[1]:2 ◌ 1 ─ goto #3 if not _2 ↑′ 2 ─ %2 = %new(Base.RefValue{String}, _3)::Base.RefValue{String} ◌ └── goto #4 ↑′ 3 ─ %4 = %new(Base.RefValue{String}, _4)::Base.RefValue{String} ↑′ 4 ┄ %5 = φ (#2 => %2, #3 => %4)::Base.RefValue{String} ◌ └── return %5
Alias Analysis
EscapeAnalysis
реализует обратный анализ полей для того, чтобы рассуждать о побегах, накладываемых на поля объектов с определенной точностью, и свойство x::EscapeInfo
's x.AliasInfo
существует для этой цели. Оно записывает все возможные значения, которые могут быть алиасированы к полям x
на "местах использования", а затем информация о побегах этих записанных значений распространяется на фактические значения полей позже на "местах определения". Более конкретно, анализ записывает значение, которое может быть алиасировано к полю объекта, анализируя вызов getfield
, а затем распространяет его информацию о побегах на поле при анализе выражения %new(...)
или вызова setfield!
[Dynamism].
julia> code_escapes((String,)) do s obj = SafeRef("init") obj[] = s v = obj[] return v end
#7(↑ s::String) in Main at REPL[1]:2 ◌ 1 ─ return _2
В приведенном примере ReturnEscape
, наложенный на %3
(соответствующий v
), не передается напрямую на %1
(соответствующий obj
), а скорее ReturnEscape
передается только на _2
(соответствующий s
). Здесь %3
записывается в свойстве AliasInfo
%1
, так как он может быть связан с первым полем %1
, и затем, при анализе Base.setfield!(%1, :x, _2)::String
, эта информация о выходе передается на _2
, но не на %1
.
Итак, EscapeAnalysis
отслеживает, какие элементы IR могут быть алиасированы в цепочке getfield
-%new
/setfield!
, чтобы проанализировать утечки полей объектов, но на самом деле этот анализ алиасов необходимо обобщить, чтобы обрабатывать и другие элементы IR. Это связано с тем, что в IR Julia один и тот же объект иногда представляется разными элементами IR, и поэтому мы должны убедиться, что эти разные элементы IR, которые на самом деле могут представлять один и тот же объект, имеют одинаковую информацию о утечках. Элементы IR, которые возвращают один и тот же объект в качестве своих операндов, такие как PiNode
и typeassert
, могут вызывать алиасинг на уровне IR и, следовательно, требуют, чтобы информация о утечках, наложенная на любые из таких алиасированных значений, была общей между ними. Более интересно, это также необходимо для правильного рассуждения о мутациях на PhiNode
. Рассмотрим следующий пример:
julia> code_escapes((Bool, String,)) do cond, x if cond ϕ2 = ϕ1 = SafeRef("foo") else ϕ2 = ϕ1 = SafeRef("bar") end ϕ2[] = x y = ϕ1[] return y end
#9(✓ cond::Bool, ↑ x::String) in Main at REPL[1]:2 ◌ 1 ─ goto #3 if not _2 ✓′ 2 ─ %2 = %new(Main.SafeRef{String}, "foo")::Main.SafeRef{String} ◌ └── goto #4 ✓′ 3 ─ %4 = %new(Main.SafeRef{String}, "bar")::Main.SafeRef{String} ✓′ 4 ┄ %5 = φ (#2 => %2, #3 => %4)::Main.SafeRef{String} ✓′ │ %6 = φ (#2 => %2, #3 => %4)::Main.SafeRef{String} ◌ │ Base.setfield!(%5, :x, _3)::String ↑ │ %8 = Base.getfield(%6, :x)::String ◌ └── return %8
ϕ1 = %5
и ϕ2 = %6
являются алиасами, и поэтому ReturnEscape
, наложенный на %8 = Base.getfield(%6, :x)::String
(соответствующий y = ϕ1[]
), необходимо распространить на Base.setfield!(%5, :x, _3)::String
(соответствующий ϕ2[] = x
). Для того чтобы такая информация об уходе была правильно распространена, анализ должен распознать, что предшественники ϕ1
и ϕ2
также могут быть алиасами и уравнять их информацию об уходе.
Одним из интересных свойств такой информации об алиасах является то, что она не известна на сайте "использования", но может быть получена только на сайте "определения" (так как алиасинг концептуально эквивалентен присваиванию), и, следовательно, она не подходит для обратного анализа. Для эффективного распространения информации об эскейпе между связанными значениями EscapeAnalysis.jl использует подход, вдохновленный алгоритмом анализа эскейпа, объясненным в старой статье по JVM[JVM05]. То есть, помимо управления элементами решетки эскейпа, анализ также поддерживает набор "эквивалентных" алиасов, раздельный набор алиасированных аргументов и операторов SSA. Набор алиасов управляет значениями, которые могут быть алиасированы друг к другу, и позволяет уравнивать информацию об эскейпе, наложенную на любое из таких алиасированных значений, между ними.
Array Analysis
Анализ алиасов для полей объектов, описанный выше, также можно обобщить для анализа операций с массивами. EscapeAnalysis
реализует обработку различных примитивных операций с массивами, чтобы он мог распространять утечки через цепочку использования-определения arrayref
-arrayset
и не утекал выделенные массивы слишком консервативно:
julia> code_escapes((String,)) do s ary = Any[] push!(ary, SafeRef(s)) return ary[1], length(ary) end
#11(X s::String) in Main at REPL[1]:2 X 1 ── %1 = Core.getproperty(Memory{Any}, :instance)::Memory{Any} X │ %2 = invoke Core.memoryref(%1::Memory{Any})::MemoryRef{Any} X │ %3 = %new(Vector{Any}, %2, (0,))::Vector{Any} X │ %4 = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String} X │ %5 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %6 = Base.getfield(%5, :mem)::Memory{Any} X │ %7 = Base.getfield(%6, :length)::Int64 X │ %8 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %9 = $(Expr(:boundscheck, true))::Bool X │ %10 = Base.getfield(%8, 1, %9)::Int64 ◌ │ %11 = Base.add_int(%10, 1)::Int64 ◌ │ %12 = Base.memoryrefoffset(%5)::Int64 X │ %13 = Core.tuple(%11)::Tuple{Int64} ◌ │ Base.setfield!(%3, :size, %13)::Tuple{Int64} ◌ │ %15 = Base.add_int(%12, %11)::Int64 ◌ │ %16 = Base.sub_int(%15, 1)::Int64 ◌ │ %17 = Base.slt_int(%7, %16)::Bool ◌ └─── goto #3 if not %17 X 2 ── %19 = %new(Base.var"#133#134"{Vector{Any}, Int64, Int64, Int64, Int64, Int64, Memory{Any}, MemoryRef{Any}}, %3, %16, %12, %11, %10, %7, %6, %5)::Base.var"#133#134"{Vector{Any}, Int64, Int64, Int64, Int64, Int64, Memory{Any}, MemoryRef{Any}} ✓ └─── invoke %19()::MemoryRef{Any} ◌ 3 ┄─ goto #4 X 4 ── %22 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %23 = $(Expr(:boundscheck, true))::Bool X │ %24 = Base.getfield(%22, 1, %23)::Int64 X │ %25 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %26 = Base.memoryrefnew(%25, %24, false)::MemoryRef{Any} X │ Base.memoryrefset!(%26, %4, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #5 ◌ 5 ── %29 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #9 if not %29 ◌ 6 ── %31 = Base.sub_int(1, 1)::Int64 ◌ │ %32 = Base.bitcast(Base.UInt, %31)::UInt64 X │ %33 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %34 = $(Expr(:boundscheck, true))::Bool X │ %35 = Base.getfield(%33, 1, %34)::Int64 ◌ │ %36 = Base.bitcast(Base.UInt, %35)::UInt64 ◌ │ %37 = Base.ult_int(%32, %36)::Bool ◌ └─── goto #8 if not %37 ◌ 7 ── goto #9 ◌ 8 ── %40 = Core.tuple(1)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %40::Tuple{Int64})::Union{} ◌ └─── unreachable X 9 ┄─ %43 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %44 = Base.memoryrefnew(%43, 1, false)::MemoryRef{Any} X │ %45 = Base.memoryrefget(%44, :not_atomic, false)::Any ◌ └─── goto #10 X 10 ─ %47 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %48 = $(Expr(:boundscheck, true))::Bool X │ %49 = Base.getfield(%47, 1, %48)::Int64 ↑′ │ %50 = Core.tuple(%45, %49)::Tuple{Any, Int64} ◌ └─── return %50
В приведенном выше примере EscapeAnalysis
понимает, что %20
и %2
(соответствующие выделенному объекту SafeRef(s)
) являются алиасами через цепочку arrayset
-arrayref
и накладывает на них ReturnEscape
, но не накладывает его на выделенный массив %1
(соответствующий ary
). EscapeAnalysis
все еще накладывает ThrownEscape
на ary
, поскольку также необходимо учитывать потенциальные утечки через BoundsError
, но также стоит отметить, что такие необработанные ThrownEscape
часто можно игнорировать при оптимизации выделения ary
.
Кроме того, в случаях, когда информация об индексах массива, а также размеры массива могут быть известны точно, EscapeAnalysis
может даже рассуждать о "поэлементном" алиасинге через цепочку arrayref
-arrayset
, так как EscapeAnalysis
выполняет анализ алиасов "по полям" для объектов:
julia> code_escapes((String,String)) do s, t ary = Vector{Any}(undef, 2) ary[1] = SafeRef(s) ary[2] = SafeRef(t) return ary[1], length(ary) end
#13(X s::String, X t::String) in Main at REPL[1]:2 X 1 ── %1 = $(Expr(:foreigncall, :(:jl_alloc_genericmemory), Ref{Memory{Any}}, svec(Any, Int64), 0, :(:ccall), Memory{Any}, 2, 2))::Memory{Any} X │ %2 = Core.memoryrefnew(%1)::MemoryRef{Any} X │ %3 = %new(Vector{Any}, %2, (2,))::Vector{Any} X │ %4 = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String} ◌ │ %5 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #5 if not %5 ◌ 2 ── %7 = Base.sub_int(1, 1)::Int64 ◌ │ %8 = Base.bitcast(Base.UInt, %7)::UInt64 X │ %9 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %10 = $(Expr(:boundscheck, true))::Bool X │ %11 = Base.getfield(%9, 1, %10)::Int64 ◌ │ %12 = Base.bitcast(Base.UInt, %11)::UInt64 ◌ │ %13 = Base.ult_int(%8, %12)::Bool ◌ └─── goto #4 if not %13 ◌ 3 ── goto #5 ◌ 4 ── %16 = Core.tuple(1)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %16::Tuple{Int64})::Union{} ◌ └─── unreachable X 5 ┄─ %19 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %20 = Base.memoryrefnew(%19, 1, false)::MemoryRef{Any} X │ Base.memoryrefset!(%20, %4, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #6 X 6 ── %23 = %new(Main.SafeRef{String}, _3)::Main.SafeRef{String} ◌ │ %24 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #10 if not %24 ◌ 7 ── %26 = Base.sub_int(2, 1)::Int64 ◌ │ %27 = Base.bitcast(Base.UInt, %26)::UInt64 X │ %28 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %29 = $(Expr(:boundscheck, true))::Bool X │ %30 = Base.getfield(%28, 1, %29)::Int64 ◌ │ %31 = Base.bitcast(Base.UInt, %30)::UInt64 ◌ │ %32 = Base.ult_int(%27, %31)::Bool ◌ └─── goto #9 if not %32 ◌ 8 ── goto #10 ◌ 9 ── %35 = Core.tuple(2)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %35::Tuple{Int64})::Union{} ◌ └─── unreachable X 10 ┄ %38 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %39 = Base.memoryrefnew(%38, 2, false)::MemoryRef{Any} X │ Base.memoryrefset!(%39, %23, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #11 ◌ 11 ─ %42 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #15 if not %42 ◌ 12 ─ %44 = Base.sub_int(1, 1)::Int64 ◌ │ %45 = Base.bitcast(Base.UInt, %44)::UInt64 X │ %46 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %47 = $(Expr(:boundscheck, true))::Bool X │ %48 = Base.getfield(%46, 1, %47)::Int64 ◌ │ %49 = Base.bitcast(Base.UInt, %48)::UInt64 ◌ │ %50 = Base.ult_int(%45, %49)::Bool ◌ └─── goto #14 if not %50 ◌ 13 ─ goto #15 ◌ 14 ─ %53 = Core.tuple(1)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %53::Tuple{Int64})::Union{} ◌ └─── unreachable X 15 ┄ %56 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %57 = Base.memoryrefnew(%56, 1, false)::MemoryRef{Any} X │ %58 = Base.memoryrefget(%57, :not_atomic, false)::Any ◌ └─── goto #16 X 16 ─ %60 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %61 = $(Expr(:boundscheck, true))::Bool X │ %62 = Base.getfield(%60, 1, %61)::Int64 ↑′ │ %63 = Core.tuple(%58, %62)::Tuple{Any, Int64} ◌ └─── return %63
Обратите внимание, что ReturnEscape
применяется только к %2
(соответствующему SafeRef(s)
), но не к %4
(соответствующему SafeRef(t)
). Это связано с тем, что размерность выделенного массива и индексы, участвующие во всех операциях arrayref
/arrayset
, доступны как постоянная информация, и EscapeAnalysis
может понять, что %6
ссылается на %2
, но никогда не будет ссылаться на %4
. В таком случае последующие оптимизационные проходы смогут заменить Base.arrayref(true, %1, 1)::Any
на %2
(так называемая "перемотка загрузки") и в конечном итоге полностью устранить выделение массива %1
(так называемая "замена скаляров").
Когда сравнивать с анализом полей объектов, где доступ к полю объекта может быть проанализирован тривиально с использованием информации о типе, полученной путем вывода, размерность массива не закодирована как информация о типе, и поэтому нам нужен дополнительный анализ для получения этой информации. EscapeAnalysis
в данный момент сначала выполняет дополнительный простой линейный проход для анализа размерностей выделенных массивов, прежде чем запустить основную рутинную процедуру анализа, чтобы последующий анализ утечек мог точно анализировать операции с этими массивами.
Однако такая точная "поэлементная" анализ алиасов часто бывает сложной. По сути, основная трудность, присущая массивам, заключается в том, что размерность массива и индекс часто являются неконстантными:
- цикл часто производит изменяющиеся, неконстантные индексы массива
- (специфично для векторов) изменение размера массива изменяет размерность массива и делает его неконстантным
Давайте обсудим эти трудности на конкретных примерах.
В следующем примере EscapeAnalysis
не проходит точный анализ алиасов, поскольку индекс в Base.arrayset(false, %4, %8, %6)::Vector{Any}
не является (тривиально) константным. Особенно Any[nothing, nothing]
образует цикл и вызывает эту операцию arrayset
в цикле, где %6
представлен как значение ϕ-узла (значение которого зависит от управления потоком). В результате ReturnEscape
накладывается на оба %23
(соответствующий SafeRef(s)
) и %25
(соответствующий SafeRef(t)
), хотя в идеале мы хотим, чтобы он накладывался только на %23
, но не на %25
:
julia> code_escapes((String,String)) do s, t ary = Any[nothing, nothing] ary[1] = SafeRef(s) ary[2] = SafeRef(t) return ary[1], length(ary) end
#15(X s::String, X t::String) in Main at REPL[1]:2 X 1 ── %1 = $(Expr(:foreigncall, :(:jl_alloc_genericmemory), Ref{Memory{Any}}, svec(Any, Int64), 0, :(:ccall), Memory{Any}, 2, 2))::Memory{Any} X │ %2 = Core.memoryrefnew(%1)::MemoryRef{Any} X └─── %3 = %new(Vector{Any}, %2, (2,))::Vector{Any} ◌ 2 ┄─ %4 = φ (#1 => 1, #6 => %14)::Int64 ◌ │ %5 = φ (#1 => 1, #6 => %15)::Int64 X │ %6 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %7 = Base.memoryrefnew(%6, %4, false)::MemoryRef{Any} ◌ │ Base.memoryrefset!(%7, nothing, :not_atomic, false)::Nothing ◌ │ %9 = (%5 === 2)::Bool ◌ └─── goto #4 if not %9 ◌ 3 ── goto #5 ◌ 4 ── %12 = Base.add_int(%5, 1)::Int64 ◌ └─── goto #5 ◌ 5 ┄─ %14 = φ (#4 => %12)::Int64 ◌ │ %15 = φ (#4 => %12)::Int64 ◌ │ %16 = φ (#3 => true, #4 => false)::Bool ◌ │ %17 = Base.not_int(%16)::Bool ◌ └─── goto #7 if not %17 ◌ 6 ── goto #2 ◌ 7 ── goto #8 X 8 ── %21 = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String} ◌ │ %22 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #12 if not %22 ◌ 9 ── %24 = Base.sub_int(1, 1)::Int64 ◌ │ %25 = Base.bitcast(Base.UInt, %24)::UInt64 X │ %26 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %27 = $(Expr(:boundscheck, true))::Bool X │ %28 = Base.getfield(%26, 1, %27)::Int64 ◌ │ %29 = Base.bitcast(Base.UInt, %28)::UInt64 ◌ │ %30 = Base.ult_int(%25, %29)::Bool ◌ └─── goto #11 if not %30 ◌ 10 ─ goto #12 ◌ 11 ─ %33 = Core.tuple(1)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %33::Tuple{Int64})::Union{} ◌ └─── unreachable X 12 ┄ %36 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %37 = Base.memoryrefnew(%36, 1, false)::MemoryRef{Any} X │ Base.memoryrefset!(%37, %21, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #13 X 13 ─ %40 = %new(Main.SafeRef{String}, _3)::Main.SafeRef{String} ◌ │ %41 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #17 if not %41 ◌ 14 ─ %43 = Base.sub_int(2, 1)::Int64 ◌ │ %44 = Base.bitcast(Base.UInt, %43)::UInt64 X │ %45 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %46 = $(Expr(:boundscheck, true))::Bool X │ %47 = Base.getfield(%45, 1, %46)::Int64 ◌ │ %48 = Base.bitcast(Base.UInt, %47)::UInt64 ◌ │ %49 = Base.ult_int(%44, %48)::Bool ◌ └─── goto #16 if not %49 ◌ 15 ─ goto #17 ◌ 16 ─ %52 = Core.tuple(2)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %52::Tuple{Int64})::Union{} ◌ └─── unreachable X 17 ┄ %55 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %56 = Base.memoryrefnew(%55, 2, false)::MemoryRef{Any} X │ Base.memoryrefset!(%56, %40, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #18 ◌ 18 ─ %59 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #22 if not %59 ◌ 19 ─ %61 = Base.sub_int(1, 1)::Int64 ◌ │ %62 = Base.bitcast(Base.UInt, %61)::UInt64 X │ %63 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %64 = $(Expr(:boundscheck, true))::Bool X │ %65 = Base.getfield(%63, 1, %64)::Int64 ◌ │ %66 = Base.bitcast(Base.UInt, %65)::UInt64 ◌ │ %67 = Base.ult_int(%62, %66)::Bool ◌ └─── goto #21 if not %67 ◌ 20 ─ goto #22 ◌ 21 ─ %70 = Core.tuple(1)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %70::Tuple{Int64})::Union{} ◌ └─── unreachable X 22 ┄ %73 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %74 = Base.memoryrefnew(%73, 1, false)::MemoryRef{Any} X │ %75 = Base.memoryrefget(%74, :not_atomic, false)::Any ◌ └─── goto #23 X 23 ─ %77 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %78 = $(Expr(:boundscheck, true))::Bool X │ %79 = Base.getfield(%77, 1, %78)::Int64 ↑′ │ %80 = Core.tuple(%75, %79)::Tuple{Any, Int64} ◌ └─── return %80
Следующий пример иллюстрирует, как изменение размера векторов усложняет точный анализ алиасов. Основная трудность заключается в том, что размер выделенного массива %1
сначала инициализируется как 0
, но затем изменяется двумя вызовами :jl_array_grow_end
. В настоящее время EscapeAnalysis
просто отказывается от точного анализа алиасов, когда сталкивается с любыми операциями изменения размера массива, и поэтому ReturnEscape
накладывается на оба %2
(соответствующий SafeRef(s)
) и %20
(соответствующий SafeRef(t)
):
julia> code_escapes((String,String)) do s, t ary = Any[] push!(ary, SafeRef(s)) push!(ary, SafeRef(t)) ary[1], length(ary) end
#17(X s::String, X t::String) in Main at REPL[1]:2 X 1 ── %1 = Core.getproperty(Memory{Any}, :instance)::Memory{Any} X │ %2 = invoke Core.memoryref(%1::Memory{Any})::MemoryRef{Any} X │ %3 = %new(Vector{Any}, %2, (0,))::Vector{Any} X │ %4 = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String} X │ %5 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %6 = Base.getfield(%5, :mem)::Memory{Any} X │ %7 = Base.getfield(%6, :length)::Int64 X │ %8 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %9 = $(Expr(:boundscheck, true))::Bool X │ %10 = Base.getfield(%8, 1, %9)::Int64 ◌ │ %11 = Base.add_int(%10, 1)::Int64 ◌ │ %12 = Base.memoryrefoffset(%5)::Int64 X │ %13 = Core.tuple(%11)::Tuple{Int64} ◌ │ Base.setfield!(%3, :size, %13)::Tuple{Int64} ◌ │ %15 = Base.add_int(%12, %11)::Int64 ◌ │ %16 = Base.sub_int(%15, 1)::Int64 ◌ │ %17 = Base.slt_int(%7, %16)::Bool ◌ └─── goto #3 if not %17 X 2 ── %19 = %new(Base.var"#133#134"{Vector{Any}, Int64, Int64, Int64, Int64, Int64, Memory{Any}, MemoryRef{Any}}, %3, %16, %12, %11, %10, %7, %6, %5)::Base.var"#133#134"{Vector{Any}, Int64, Int64, Int64, Int64, Int64, Memory{Any}, MemoryRef{Any}} ✓ └─── invoke %19()::MemoryRef{Any} ◌ 3 ┄─ goto #4 X 4 ── %22 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %23 = $(Expr(:boundscheck, true))::Bool X │ %24 = Base.getfield(%22, 1, %23)::Int64 X │ %25 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %26 = Base.memoryrefnew(%25, %24, false)::MemoryRef{Any} X │ Base.memoryrefset!(%26, %4, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #5 X 5 ── %29 = %new(Main.SafeRef{String}, _3)::Main.SafeRef{String} X │ %30 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %31 = Base.getfield(%30, :mem)::Memory{Any} X │ %32 = Base.getfield(%31, :length)::Int64 X │ %33 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %34 = $(Expr(:boundscheck, true))::Bool X │ %35 = Base.getfield(%33, 1, %34)::Int64 ◌ │ %36 = Base.add_int(%35, 1)::Int64 ◌ │ %37 = Base.memoryrefoffset(%30)::Int64 X │ %38 = Core.tuple(%36)::Tuple{Int64} ◌ │ Base.setfield!(%3, :size, %38)::Tuple{Int64} ◌ │ %40 = Base.add_int(%37, %36)::Int64 ◌ │ %41 = Base.sub_int(%40, 1)::Int64 ◌ │ %42 = Base.slt_int(%32, %41)::Bool ◌ └─── goto #7 if not %42 X 6 ── %44 = %new(Base.var"#133#134"{Vector{Any}, Int64, Int64, Int64, Int64, Int64, Memory{Any}, MemoryRef{Any}}, %3, %41, %37, %36, %35, %32, %31, %30)::Base.var"#133#134"{Vector{Any}, Int64, Int64, Int64, Int64, Int64, Memory{Any}, MemoryRef{Any}} ✓ └─── invoke %44()::MemoryRef{Any} ◌ 7 ┄─ goto #8 X 8 ── %47 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %48 = $(Expr(:boundscheck, true))::Bool X │ %49 = Base.getfield(%47, 1, %48)::Int64 X │ %50 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %51 = Base.memoryrefnew(%50, %49, false)::MemoryRef{Any} X │ Base.memoryrefset!(%51, %29, :not_atomic, false)::Main.SafeRef{String} ◌ └─── goto #9 ◌ 9 ── %54 = $(Expr(:boundscheck, true))::Bool ◌ └─── goto #13 if not %54 ◌ 10 ─ %56 = Base.sub_int(1, 1)::Int64 ◌ │ %57 = Base.bitcast(Base.UInt, %56)::UInt64 X │ %58 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %59 = $(Expr(:boundscheck, true))::Bool X │ %60 = Base.getfield(%58, 1, %59)::Int64 ◌ │ %61 = Base.bitcast(Base.UInt, %60)::UInt64 ◌ │ %62 = Base.ult_int(%57, %61)::Bool ◌ └─── goto #12 if not %62 ◌ 11 ─ goto #13 ◌ 12 ─ %65 = Core.tuple(1)::Tuple{Int64} ✓ │ invoke Base.throw_boundserror(%3::Vector{Any}, %65::Tuple{Int64})::Union{} ◌ └─── unreachable X 13 ┄ %68 = Base.getfield(%3, :ref)::MemoryRef{Any} X │ %69 = Base.memoryrefnew(%68, 1, false)::MemoryRef{Any} X │ %70 = Base.memoryrefget(%69, :not_atomic, false)::Any ◌ └─── goto #14 X 14 ─ %72 = Base.getfield(%3, :size)::Tuple{Int64} ◌ │ %73 = $(Expr(:boundscheck, true))::Bool X │ %74 = Base.getfield(%72, 1, %73)::Int64 ↑′ │ %75 = Core.tuple(%70, %74)::Tuple{Any, Int64} ◌ └─── return %75
Чтобы справиться с этими трудностями, нам нужно, чтобы вывод был осведомлён о размерах массивов и передавал размеры массивов в зависимости от потока[ArrayDimension], а также придумал хорошее представление для значений, зависящих от цикла.
EscapeAnalysis
в данный момент быстро переключается на более неточный анализ, который не отслеживает точную информацию об индексах в случаях, когда размеры массивов или индексы тривиально не константны. Переключение можно естественно реализовать как операцию объединения решеток свойства EscapeInfo.AliasInfo
в рамках анализа потоков данных.
Exception Handling
Стоит также отметить, как EscapeAnalysis
обрабатывает возможные утечки через исключения. Наивно кажется, что достаточно распространить информацию об утечках, наложенную на объект :the_exception
, на все значения, которые могут быть выброшены в соответствующем блоке try
. Но на самом деле существует несколько других способов доступа к объекту исключения в Julia, таких как Base.current_exceptions
и rethrow
. Например, анализ утечек должен учитывать потенциальную утечку r
в приведенном ниже примере:
julia> const GR = Ref{Any}();
julia> @noinline function rethrow_escape!() try rethrow() catch err GR[] = err end end;
julia> get′(x) = isassigned(x) ? x[] : throw(x);
julia> code_escapes() do r = Ref{String}() local t try t = get′(r) catch err t = typeof(err) # `err` (which `r` aliases to) doesn't escape here rethrow_escape!() # but `r` escapes here end return t end
#19() in Main at REPL[4]:2 X 1 ─ %1 = %new(Base.RefValue{String})::Base.RefValue{String} ◌ 2 ─ %2 = enter #8 ◌ 3 ─ %3 = Base.isdefined(%1, :x)::Bool ◌ └── goto #5 if not %3 X 4 ─ %5 = Base.getfield(%1, :x)::String ◌ └── goto #6 ◌ 5 ─ Main.throw(%1)::Union{} ◌ └── unreachable ◌ 6 ─ $(Expr(:leave, :(%2))) ◌ 7 ─ goto #9 ✓ 8 ┄ %11 = $(Expr(:the_exception))::Any X │ %12 = Main.typeof(%11)::DataType ✓ │ invoke Main.rethrow_escape!()::Any ◌ └── $(Expr(:pop_exception, :(%2)))::Core.Const(nothing) X 9 ┄ %15 = φ (#7 => %5, #8 => %12)::Union{DataType, String} ◌ └── return %15
Требуется глобальный анализ, чтобы правильно рассуждать обо всех возможных выходах через существующие интерфейсы исключений. На данный момент мы всегда консервативно передаем информацию о самом верхнем выходе всем потенциально выбрасываемым объектам, поскольку такой дополнительный анализ может не стоить того, учитывая, что обработка исключений и пути ошибок обычно не требуют высокой производительности, а также оптимизации путей ошибок могут быть очень неэффективными, поскольку они часто даже "неоптимизированы" намеренно по причинам задержки.
x::EscapeInfo
свойство x.ThrownEscape
записывает SSA-операторы, где x
может быть выброшен как исключение. Используя эту информацию, EscapeAnalysis
может ограниченно распространять возможные утечки через исключения только на те, которые могут быть выброшены в каждом регионе try
:
julia> result = code_escapes((String,String)) do s1, s2 r1 = Ref(s1) r2 = Ref(s2) local ret try s1 = get′(r1) ret = sizeof(s1) catch err global GV = err # will definitely escape `r1` end s2 = get′(r2) # still `r2` doesn't escape fully return s2 end
#21(X s1::String, ↑ s2::String) in Main at REPL[1]:2 X 1 ── %1 = %new(Base.RefValue{String}, _2)::Base.RefValue{String} *′ └─── %2 = %new(Base.RefValue{String}, _3)::Base.RefValue{String} *′ 2 ── %3 = ϒ (%2)::Base.RefValue{String} ◌ └─── %4 = enter #8 ◌ 3 ── %5 = Base.isdefined(%1, :x)::Bool ◌ └─── goto #5 if not %5 X 4 ── Base.getfield(%1, :x)::String ◌ └─── goto #6 ◌ 5 ── Main.throw(%1)::Union{} ◌ └─── unreachable ◌ 6 ── $(Expr(:leave, :(%4))) ◌ 7 ── goto #11 *′ 8 ┄─ %13 = φᶜ (%3)::Base.RefValue{String} X └─── %14 = $(Expr(:the_exception))::Any ◌ 9 ── nothing::Nothing ◌ 10 ─ (Main.GV = %14)::Any ◌ └─── $(Expr(:pop_exception, :(%4)))::Core.Const(nothing) *′ 11 ┄ %18 = φ (#7 => %2, #10 => %13)::Base.RefValue{String} ◌ │ %19 = Base.isdefined(%18, :x)::Bool ◌ └─── goto #13 if not %19 ↑ 12 ─ %21 = Base.getfield(%18, :x)::String ◌ └─── goto #14 ◌ 13 ─ Main.throw(%18)::Union{} ◌ └─── unreachable ◌ 14 ─ return %21
Analysis Usage
analyze_escapes
является точкой входа для анализа информации об экранировании элементов SSA-IR.
Большинство оптимизаций, таких как SROA (sroa_pass!
), более эффективны, когда применяются к оптимизированному исходному коду, который упрощен проходом инлайнинга (ssa_inlining_pass!
) за счет разрешения межпроцедурных вызовов и расширения источников вызываемых функций. Соответственно, analyze_escapes
также может анализировать IR после инлайнинга и собирать информацию об утечках, которая полезна для определенных оптимизаций, связанных с памятью.
Однако, поскольку определенные оптимизационные проходы, такие как инлайнинг, могут изменять потоки управления и устранять мертвый код, они могут нарушить межпроцедурную корректность информации о выходе. В частности, для того чтобы собрать межпроцедурно корректную информацию о выходе, нам необходимо проанализировать IR до инлайнинга.
По этой причине analyze_escapes
может анализировать IRCode
на любом уровне оптимизации Julia, и особенно его предполагается использовать на следующих двух этапах:
IPO EA
: анализировать IR до инлайнинга для генерации кэша информации об эскейпах, действительного для IPOLocal EA
: анализировать IR после инлайнинга для сбора локально-валидной информации об утечках
Информация о побегах, полученная с помощью IPO EA
, преобразуется в структуру данных ArgEscapeCache
и кэшируется глобально. Передавая соответствующий обратный вызов get_escape_cache
в analyze_escapes
, анализ побегов может улучшить точность анализа, используя кэшированную межпроцедурную информацию о неинлайненных вызываемых функциях, полученную в результате предыдущего IPO EA
. Более интересно, что также допустимо использовать информацию о побегах IPO EA
для вывода типов, например, точность вывода может быть улучшена за счет формирования Const
/PartialStruct
/MustAlias
изменяемого объекта.
Core.Compiler.EscapeAnalysis.analyze_escapes
— Functionanalyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> estate::EscapeState
Анализирует информацию об эскапах в ir
:
nargs
: количество фактических аргументов анализируемого вызоваget_escape_cache(::MethodInstance) -> Union{Bool,ArgEscapeCache}
: извлекает кэшированную информацию об эскапах аргументов
Core.Compiler.EscapeAnalysis.EscapeState
— Typeestate::EscapeState
Расширенная решетка, которая сопоставляет аргументы и значения SSA с информацией о выходе, представленной как EscapeInfo
. Информация о выходе, наложенная на элемент SSA IR x
, может быть получена с помощью estate[x]
.
Core.Compiler.EscapeAnalysis.EscapeInfo
— Typex::EscapeInfo
Латтеца для информации об уходе, которая содержит следующие свойства:
x.Analyzed::Bool
: не является формально частью латтецы, только указывает, была ли проанализированаx
x.ReturnEscape::Bool
: указывает, чтоx
может уйти к вызывающему через возвратx.ThrownEscape::BitSet
: записывает номера операторов SSA, гдеx
может быть выброшен как исключение:isempty(x.ThrownEscape)
:x
никогда не будет выброшен в этом кадре вызова (низ)pc ∈ x.ThrownEscape
:x
может быть выброшен на операторе SSA вpc
-1 ∈ x.ThrownEscape
:x
может быть выброшен в произвольных точках этого кадра вызова (верх)
Эта информация будет использоваться
escape_exception!
для распространения потенциальных уходов через исключение.x.AliasInfo::Union{Bool,IndexableFields,IndexableElements,Unindexable}
: поддерживает все возможные значения, которые могут быть алиасированы к полям или элементам массиваx
:x.AliasInfo === false
указывает, что поля/элементыx
еще не были проанализированыx.AliasInfo === true
указывает, что поля/элементыx
не могут быть проанализированы, например, типx
не известен или не является конкретным, и, следовательно, его поля/элементы не могут быть точно известныx.AliasInfo::IndexableFields
записывает все возможные значения, которые могут быть алиасированы к полям объектаx
с точной информацией об индексахx.AliasInfo::IndexableElements
записывает все возможные значения, которые могут быть алиасированы к элементам массиваx
с точной информацией об индексахx.AliasInfo::Unindexable
записывает все возможные значения, которые могут быть алиасированы к полям/элементамx
без точной информации об индексах
x.Liveness::BitSet
: записывает номера операторов SSA, гдеx
должен быть жив, например, чтобы быть использованным как аргумент вызова, чтобы быть возвращенным вызывающему или сохраненным для:foreigncall
:isempty(x.Liveness)
:x
никогда не будет использован в этом кадре вызова (низ)0 ∈ x.Liveness
также имеет специальное значение, что это аргумент вызова текущего анализируемого кадра вызова (и, следовательно, он виден из вызывающего немедленно).pc ∈ x.Liveness
:x
может быть использован на операторе SSA вpc
-1 ∈ x.Liveness
:x
может быть использован в произвольных точках этого кадра вызова (верх)
Существуют утилитарные конструкторы для создания общих EscapeInfo
, например,
NoEscape()
: нижний (подобный) элемент этой латтецы, означающий, что он не уйдет никудаAllEscape()
: самый верхний элемент этой латтецы, означающий, что он уйдет повсюду
analyze_escapes
будет переводить эти элементы снизу вверх, в том же направлении, что и родная процедура вывода типов Julia. Абстрактное состояние будет инициализировано нижними (подобными) элементами:
- аргументы вызова инициализируются как
ArgEscape()
, чье свойствоLiveness
включает0
, чтобы указать, что он передается как аргумент вызова и виден из вызывающего немедленно - другие состояния инициализируются как
NotAnalyzed()
, что является специальным элементом латтецы, который немного нижеNoEscape
, но в то же время не представляет никакого значения, кроме того, что он еще не проанализирован (поэтому он формально не является частью латтецы)
- LatticeDesignOur type inference implementation takes the alternative approach, where each lattice property is represented by a special lattice element type object. It turns out that it started to complicate implementations of the lattice operations mainly because it often requires conversion rules between each lattice element type object. And we are working on overhauling our type inference lattice implementation with
EscapeInfo
-like lattice design. - MM02A Graph-Free approach to Data-Flow Analysis. Markas Mohnen, 2002, April. https://api.semanticscholar.org/CorpusID:28519618.
- BackandForthOur type inference algorithm in contrast is implemented as a forward analysis, because type information usually flows from "definition" to "usage" and it is more natural and effective to propagate such information in a forward way.
- DynamismIn some cases, however, object fields can't be analyzed precisely. For example, object may escape to somewhere
EscapeAnalysis
can't account for possible memory effects on it, or fields of the objects simply can't be known because of the lack of type information. In such casesAliasInfo
property is raised to the topmost element within its own lattice order, and it causes succeeding field analysis to be conservative and escape information imposed on fields of an unanalyzable object to be propagated to the object itself. - JVM05Escape Analysis in the Context of Dynamic Compilation and Deoptimization. Thomas Kotzmann and Hanspeter Mössenböck, 2005, June. https://dl.acm.org/doi/10.1145/1064979.1064996.
- ArrayDimensionOtherwise we will need yet another forward data-flow analysis on top of the escape analysis.