EscapeAnalysis
Core.Compiler.EscapeAnalysis
— это служебный модуль компилятора, предназначенный для анализа информации о выходе для IR SSA-формы Julia, также известной как IRCode
.
Цель этого escape-анализа заключается в следующем:
-
использование высокоуровневой семантики Julia, понимание действий выходов и назначения псевдонимов с помощью межпроцедурных вызовов;
-
быть достаточно универсальным, чтобы использоваться для различных оптимизаций, включая SROA с поддержкой псевдонимов, заблаговременную вставку
finalize
, построениеImmutableArray
без копирования, выделение стека изменяемых объектов и т. д.; -
выполнение простой реализации на основе реализации полностью обратного анализа потока данных, а также новой конструкции решетки, сочетающей свойства ортогональной решетки.
Попробуйте!
Вы можете попробовать выполнить escape-анализ, загрузив скрипт EAUtils.jl
, который определяет подходящие записи code_escapes
и @code_escapes
для тестирования и отладки.
julia> let JULIA_DIR = normpath(Sys.BINDIR, "..", "share", "julia")
# загружает модуль `EscapeAnalysis` для определения основного кода анализа
include(normpath(JULIA_DIR, "base", "compiler", "ssair", "EscapeAnalysis", "EscapeAnalysis.jl"))
using .EscapeAnalysis
# загружает `EAUtils` для определения служебных программ
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 # обязательно выйдет из `r1`
end
s2 = get′(r2) # все еще не полностью выходит из `r2`
s3 = get′(r3) # все еще не полностью выходит из `r3`
s4 = sizeof(s4) # Аргумент `s4` здесь не выходит
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 имеют следующие значения.
-
◌
(одноцветный): это значение не анализируется, поскольку его информация о выходе не будет использоваться (например, когда объект имеет тип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
(красный): это значение может выйти в любое место, о котором escape-анализ не может судить, например выйти в глобальную память (содержитhas_all_escape(result.state[x])
). -
*
(жирный шрифт): состояние выхода этого значения находится междуReturnEscape
иAllEscape
в частичном порядкеEscapeInfo
, окрашивается в желтый цвет, если оно также имеет необработанный сгенерированный выход (содержитhas_thrown_escape(result.state[x])
). -
′
: это значение имеет дополнительную информацию о поле объекта или элементе массива в своем свойствеAliasInfo
.
Информацию о выходе каждого аргумента вызова и значения SSA можно проверить программно, как показано далее.
julia> result.state[Core.Argument(3)] # Получить EscapeInfo для `s2`
ReturnEscape
julia> result.state[Core.SSAValue(3)] # Получить EscapeInfo для `r3`
NoEscape′
Структура анализа
Конструкция решетки
EscapeAnalysis
реализуется как анализ потока данных, работающий в решетке x::EscapeInfo
, которая состоит из следующих свойств.
-
x.Analyzed::Bool
: формально не является частью решетки, только указывает, был ли проанализированx
. -
x.ReturnEscape::BitSet
: записывает операторы SSA, гдеx
может перейти к вызывающей стороне через возврат. -
x.ThrownEscape::BitSet
: записывает операторы SSA, гдеx
может быть вызван как исключение (используется для обработки исключений, описанной ниже). -
x.AliasInfo
: хранит все возможные значения, которые могут быть псевдонимами полей или элементовx
(используется для анализа псевдонимов, описанного ниже). -
x.ArgEscape::Int
(пока не реализовано): указывает, что выйдет к вызывающей стороне черезsetfield!
в аргументах.
Эти атрибуты могут быть объединены для создания частичной решетки с конечной высотой, учитывая тот инвариант, что входная программа имеет конечное число операторов благодаря семантике Julia. Самое интересное в этой конструкции решетки заключается в том, что она позволяет упростить реализацию операций решетки, разрешая обрабатывать каждое свойство решетки отдельно[1].
Распространение обратного выхода
Эта реализация escape-анализа основана на алгоритме потока данных, описанном в работе[2]. Анализ работает в решетке EscapeInfo
и переходит по элементам решетки снизу вверх, пока каждый элемент решетки не сойдется в фиксированной точке, сохраняя (концептуальный) рабочий набор, который содержит счетчики программы, соответствующие оставшимся операторам SSA, подлежащим анализу. Анализ управляет одним глобальным состоянием, которое отслеживает EscapeInfo
каждого аргумента и оператора SSA. Также отметим, что некоторая зависимость от потока закодирована в виде счетчиков программы, записанных в свойстве ReturnEscape
EscapeInfo
, которое позже может быть объединено с анализом доминирования, чтобы говорить о зависимости от потока, если это необходимо.
Отличительной особенностью этого escape-анализа является то, что он является полностью обратным, то есть информация о выходе идет от использования к определениям. Например, в приведенном ниже фрагменте кода 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
Важное замечание касается того, что этот обратный анализ позволяет информации о выходе естественным образом идти по цепочке «использование — определение», а не по порядку выполнения[3]. В результате эта схема обеспечивает простую реализацию escape-анализа. Например, 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
Анализ псевдонимов
EscapeAnalysis
реализует обратный анализ поля, чтобы с определенной точностью рассуждать о выходах, введенных в отношении полей объекта; для этого существует свойство x.AliasInfo
в x::EscapeInfo
. Он записывает все возможные значения, которые могут быть присвоены полям x
в местах использования, а затем информация о выходе этих записанных значений распространяется на фактические значения полей в местах определения. Если говорить более конкретно, анализ записывает значение, которое может быть назначено в качестве псевдонима полю объекта при анализе вызова getfield
, а затем распространяет свою информацию о выходе на поля при анализе выражения %new(...)
или вызова setfield!
[4].
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 использует подход, основанный на алгоритме escape-анализа, описанном в старой работе о JVM[5]. То есть в дополнение к управлению элементами решетки выхода анализ также поддерживает набор эквивалентных псевдонимов — несвязанное множество аргументов и операторов SSA с назначенными псевдонимами. Набор псевдонимов управляет значениями, которые могут быть назначены как псевдонимы друг другу, и позволяет выравнивать между ними информацию о выходе, наложенную на любое такое значение с псевдонимом.
Анализ массивов
Анализ псевдонимов для полей объектов, описанный выше, также может быть обобщен для анализа операций с массивами. 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
в этот момент сначала выполняет дополнительное простое линейное сканирование для анализа измерений выделенных массивов перед запуском основной процедуры анализа, чтобы последующий escape-анализ мог точно проанализировать операции с этими массивами.
Однако такой точный поэлементный анализ псевдонимов зачастую затруднителен. По сути, основная трудность, присущая массиву, заключается в том, что измерение и индекс массива часто непостоянны.
-
Цикл часто производит изменяемые в цикле непостоянные индексы массивов.
-
Характерно для векторов: изменение размера массива изменяет измерение массива и нарушает его постоянство.
Обсудим эти трудности на конкретных примерах.
В следующем примере 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
Для решения этих проблем нужно, чтобы вывод знал об измерениях массива и распространял их зависящим от порядка способом[6], а также определил корректное представление переменных значений цикла.
EscapeAnalysis
в этот момент быстро переключается на более неточный анализ, который не отслеживает точную информацию об индексах в случаях, когда измерения массива или индексы тривиально непостоянны. Переключение может быть естественным образом реализовано как операция соединения решетки свойства EscapeInfo.AliasInfo
в рамках анализа потока данных.
Обработка исключений
Также стоит обратить внимание на то, как EscapeAnalysis
обрабатывает возможные выходы через исключения. Наивно полагать, что достаточно распространить информацию о выходе, наложенную на объект :the_exception
, на все значения, которые могут появляться в соответствующем блоке try
. На самом же деле в Julia существует несколько других способов доступа к объекту исключения, например Base.current_exceptions
и rethrow
. Например, escape-анализ должен учитывать потенциальный выход 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` (псевдонимом которого является `r`) здесь не выходит
rethrow_escape!() # Но здесь выходит `r`
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 # обязательно выйдет из `r1`
end
s2 = get′(r2) # все еще не полностью выходит из `r2`
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
Использование анализа
analyze_escapes
является точкой входа для анализа информации о выходе элементов SSA-IR.
Большинство оптимизаций, таких как SROA (sroa_pass!
), более эффективны при применении к оптимизированному источнику, который был упрощен в процессе встраивания (ssa_inlining_pass!
) путем разрешения межпроцедурных вызовов и расширения источников вызываемого объекта. Соответственно, analyze_escapes
также может анализировать результаты после встраивания IR и собирать информацию о выходе, которая полезна для определенных оптимизаций, связанных с памятью.
Однако, поскольку некоторые оптимизационные процедуры, такие как встраивание, могут изменить порядок выполнения и исключить нерабочий код, они могут нарушить межпроцедурную достоверность информации о выходе. В частности, чтобы собрать допустимую межпроцедурную информацию о выходе, необходимо проанализировать предварительное встраивание IR.
По этой причине функция analyze_escapes
может анализировать IRCode
на любом этапе оптимизации на уровне Julia, и, в частности, ее предполагается использовать на следующих двух этапах.
-
IPO EA
: анализ предварительного встраивания IR для создания кэша допустимой межпроцедурной информации о выходе -
Local EA
: анализ результатов после встраивания IR для сбора локально допустимой информации о выходе
Информация о выходе, полученная с помощью IPO EA
, преобразуется в структуру данных ArgEscapeCache
и кэшируется на глобальном уровне. Передавая соответствующий обратный вызов get_escape_cache
функции analyze_escapes
, escape-анализ может повысить точность анализа, используя кэшированную межпроцедурную информацию о невстроенных вызываемых объектах, которая была получена в ходе предыдущего анализа IPO EA
. Более интересным является тот факт, что для вывода типов также допустимо использовать информацию о выходе IPO EA
. Например, точность вывода может быть улучшена путем формирования Const
/PartialStruct
/MustAlias
изменяемого объекта.
#
Core.Compiler.EscapeAnalysis.analyze_escapes
— Function
analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> estate::EscapeState
Анализирует информацию о выходе в ir
:
-
nargs
: количество фактических аргументов анализируемого вызова -
get_escape_cache(::MethodInstance) -> Union{Bool,ArgEscapeCache}
: получает кэшированную информацию о выходе аргумента
#
Core.Compiler.EscapeAnalysis.EscapeState
— Type
estate::EscapeState
Расширенная решетка, сопоставляющая аргументы и значения SSA с информацией о выходе, представленной в виде EscapeInfo
. Информация о выходе, относящаяся к элементу IR SSA x
, может быть получена с помощью estate[x]
.
#
Core.Compiler.EscapeAnalysis.EscapeInfo
— Type
x::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
, но в то же время не представляет никакого значения, кроме того, что он еще не проанализирован (таким образом, формально он не является частью решетки).
EscapeInfo
-подобной конструкцией решетки.
EscapeAnalysis
не сможет учесть возможное влияние памяти на него, или поля объектов просто могут быть неизвестны из-за отсутствия информации о типе. В таких случаях свойство AliasInfo
поднимается до самого верхнего элемента в пределах собственного порядка решетки, что приводит к более консервативному последующему анализу полей, а информация о выходе в отношении полей неанализируемого объекта распространяется на сам объект.