EscapeAnalysis

Core.Compiler.EscapeAnalysis هو وحدة أدوات المترجم التي تهدف إلى تحليل معلومات الهروب لـ Julia's SSA-form IR والمعروفة أيضًا باسم IRCode.

تهدف هذه التحليل للهروب إلى:

  • استفد من دلالات جوليا عالية المستوى، خاصةً في التفكير حول الهروب والتشابه من خلال المكالمات بين الإجراءات.
  • كن متعدد الاستخدامات بما يكفي ليتم استخدامه في تحسينات متنوعة بما في ذلك alias-aware SROA، early finalize insertion، copy-free ImmutableArray 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))::Any9 ── nothing::Nothing10 ─ (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]:21 ─      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]:21 ─     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]:21 ─      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))::Any9 ──       nothing::Nothing10 ─       (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 قبل الإدراج لتوليد ذاكرة تخزين معلومات الهروب الصالحة لـ IPO
  • Local EA: تحليل IR بعد الإدراج لجمع معلومات الهروب الصالحة محليًا

تُحوَّل المعلومات المستمدة من IPO EA إلى بنية بيانات ArgEscapeCache وتُخزَّن عالميًا. من خلال تمرير رد نداء get_escape_cache المناسب إلى analyze_escapes، يمكن لتحليل الهروب تحسين دقة التحليل من خلال الاستفادة من المعلومات بين الإجراءات المخزنة عن المتصلين غير المدمجين التي تم الحصول عليها من IPO EA السابقة. والأكثر إثارة للاهتمام، أنه من الصحيح أيضًا استخدام معلومات هروب IPO EA لاستنتاج النوع، على سبيل المثال، يمكن تحسين دقة الاستنتاج من خلال تشكيل Const/PartialStruct/MustAlias للكائن القابل للتغيير.

Core.Compiler.EscapeAnalysis.analyze_escapesFunction
analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> estate::EscapeState

تحلل معلومات الهروب في ir:

  • nargs: عدد الوسائط الفعلية لاستدعاء التحليل
  • get_escape_cache(::MethodInstance) -> Union{Bool,ArgEscapeCache}: يسترجع معلومات هروب الوسائط المخزنة في الذاكرة
source
Core.Compiler.EscapeAnalysis.EscapeStateType
estate::EscapeState

شبكة موسعة تربط المعاملات وقيم SSA بمعلومات الهروب الممثلة كـ EscapeInfo. يمكن استرجاع معلومات الهروب المفروضة على عنصر SSA IR x بواسطة estate[x].

source
Core.Compiler.EscapeAnalysis.EscapeInfoType
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 في نقاط عشوائية من إطار الاستدعاء هذا (الأعلى)

هناك منشئات مساعدة لإنشاء EscapeInfos الشائعة، على سبيل المثال،

  • NoEscape(): العنصر السفلي (مثل) لهذه الشبكة، مما يعني أنه لن يهرب إلى أي مكان
  • AllEscape(): العنصر الأعلى في هذه الشبكة، مما يعني أنه سيهرب إلى كل مكان

ستقوم analyze_escapes بنقل هذه العناصر من الأسفل إلى الأعلى، في نفس الاتجاه مثل روتين استنتاج النوع الأصلي لجوليا. سيتم تهيئة حالة مجردة مع العناصر السفلية (مثل):

  • يتم تهيئة حجج الاستدعاء كـ ArgEscape(), والتي تتضمن خاصية Liveness الخاصة بها 0 للإشارة إلى أنه يتم تمريره كحجة استدعاء ومرئي من المتصل على الفور
  • يتم تهيئة الحالات الأخرى كـ NotAnalyzed(), وهو عنصر شبكة خاص يكون أقل قليلاً من NoEscape، ولكنه في نفس الوقت لا يمثل أي معنى بخلاف أنه لم يتم تحليله بعد (لذا فهو ليس جزءًا رسميًا من الشبكة)
source

  • 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 cases AliasInfo 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.