EscapeAnalysis
Core.Compiler.EscapeAnalysis
هو وحدة أدوات المترجم التي تهدف إلى تحليل معلومات الهروب لـ Julia's SSA-form IR والمعروفة أيضًا باسم IRCode
.
تهدف هذه التحليل للهروب إلى:
- استفد من دلالات جوليا عالية المستوى، خاصةً في التفكير حول الهروب والتشابه من خلال المكالمات بين الإجراءات.
- كن متعدد الاستخدامات بما يكفي ليتم استخدامه في تحسينات متنوعة بما في ذلك 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])
صحيح)*
(bold): حالة الهروب لهذه القيمة تقع بينReturnEscape
وAllEscape
في الترتيب الجزئي لـEscapeInfo
، ملون بالأصفر إذا كان لديه هروب غير معالج أيضًا (has_thrown_escape(result.state[x])
holds)′
: هذه القيمة تحتوي على معلومات إضافية عن حقل الكائن / عنصر المصفوفة في خاصية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!
على المعاملات.
يمكن دمج هذه السمات لإنشاء شبكة جزئية لها ارتفاع محدود، مع الأخذ في الاعتبار أن البرنامج المدخل يحتوي على عدد محدود من العبارات، وهو ما تضمنه دلالات جوليا. الجزء الذكي في تصميم هذه الشبكة هو أنه يتيح تنفيذًا أبسط لعمليات الشبكة من خلال السماح لها بالتعامل مع كل خاصية من خصائص الشبكة بشكل منفصل[LatticeDesign].
Backward Escape Propagation
تستند هذه التنفيذ لتحليل الهروب إلى خوارزمية تدفق البيانات الموضحة في الورقة[MM02]. يعمل التحليل على شبكة EscapeInfo
وينتقل بين عناصر الشبكة من الأسفل إلى الأعلى حتى يتم تقارب كل عنصر في الشبكة إلى نقطة ثابتة من خلال الحفاظ على مجموعة عمل (تصورية) تحتوي على عدادات البرنامج المقابلة لبيانات SSA المتبقية التي يجب تحليلها. يدير التحليل حالة عالمية واحدة تتعقب EscapeInfo
لكل وسيط وبيان SSA، ولكن يجب أيضًا ملاحظة أن بعض حساسية التدفق مشفرة كعدادات برنامج مسجلة في خاصية ReturnEscape
لـ EscapeInfo
، والتي يمكن دمجها مع تحليل الهيمنة لاحقًا للتفكير في حساسية التدفق إذا لزم الأمر.
تصميم مميز لهذا التحليل الهروب هو أنه عكسي بالكامل، أي أن معلومات الهروب تتدفق من الاستخدامات إلى التعريفات. على سبيل المثال، في مقتطف الشيفرة أدناه، يقوم تحليل الهروب أولاً بتحليل العبارة 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
لـ 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 لجوليا، يتم أحيانًا تمثيل نفس الكائن بواسطة عناصر 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
المقابلة. ولكن هناك في الواقع عدة طرق أخرى للوصول إلى كائن الاستثناء في جوليا، مثل 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.ThrownEscape
في x::EscapeInfo
بيانات 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
s الشائعة، على سبيل المثال،
NoEscape()
: العنصر السفلي (مثل) لهذه الشبكة، مما يعني أنه لن يهرب إلى أي مكانAllEscape()
: العنصر الأعلى في هذه الشبكة، مما يعني أنه سيهرب إلى كل مكان
ستقوم analyze_escapes
بنقل هذه العناصر من الأسفل إلى الأعلى، في نفس الاتجاه مثل روتين استنتاج النوع الأصلي لجوليا. سيتم تهيئة حالة مجردة مع العناصر السفلية (مثل):
- يتم تهيئة حجج الاستدعاء كـ
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.