EscapeAnalysis
Core.Compiler.EscapeAnalysis — это модуль утилиты компилятора, который предназначен для анализа информации о выходе Julia's SSA-form IR, также известного как IRCode.
Этот анализ побега направлен на:
- использовать высокоуровневую семантику Julia, особенно рассматривать побеги и алиасинг через межпроцедурные вызовы
- быть достаточно универсальным, чтобы использоваться для различных оптимизаций, включая alias-aware SROA, early
finalizeinsertion, copy-freeImmutableArrayconstruction, стековое выделение изменяемых объектов и так далее. - достигнуть простой реализации на основе полностью обратного анализа данных, а также нового проектирования решётки, которое сочетает в себе ортогональные свойства решётки
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 endjulia> mutable struct SafeRef{T} x::T endjulia> 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`ReturnEscapejulia> 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: не является формально частью латтецы, только указывает, была ли проанализированаxx.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
EscapeAnalysiscan'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 casesAliasInfoproperty 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.