Inference
How inference works
في مترجم جوليا، تشير "استنتاج النوع" إلى عملية استنتاج أنواع القيم اللاحقة من أنواع قيم الإدخال. تم وصف نهج جوليا في الاستنتاج في المشاركات المدونة أدناه:
- Shows a simplified implementation of the data-flow analysis algorithm, that Julia's type inference routine is based on.
- Gives a high level view of inference with a focus on its inter-procedural convergence guarantee.
- Explains a refinement on the algorithm introduced in 2.
Debugging compiler.jl
يمكنك بدء جلسة جوليا، وتحرير compiler/*.jl
(على سبيل المثال لإدراج عبارات print
)، ثم استبدال Core.Compiler
في جلستك الجارية عن طريق الانتقال إلى base
وتنفيذ include("compiler/compiler.jl")
. هذه الحيلة تؤدي عادةً إلى تطوير أسرع بكثير مما لو كنت تعيد بناء جوليا مع كل تغيير.
بدلاً من ذلك، يمكنك استخدام حزمة Revise.jl لتتبع تغييرات المترجم باستخدام الأمر Revise.track(Core.Compiler)
في بداية جلسة جوليا الخاصة بك. كما هو موضح في Revise documentation، ستنعكس التعديلات على المترجم عندما يتم حفظ الملفات المعدلة.
نقطة دخول مريحة إلى الاستدلال هي typeinf_code
. إليك عرض توضيحي لتشغيل الاستدلال على convert(Int, UInt(1))
:
# Get the method
atypes = Tuple{Type{Int}, UInt} # argument types
mths = methods(convert, atypes) # worth checking that there is only one
m = first(mths)
# Create variables needed to call `typeinf_code`
interp = Core.Compiler.NativeInterpreter()
sparams = Core.svec() # this particular method doesn't have type-parameters
run_optimizer = true # run all inference optimizations
types = Tuple{typeof(convert), atypes.parameters...} # Tuple{typeof(convert), Type{Int}, UInt}
Core.Compiler.typeinf_code(interp, m, types, sparams, run_optimizer)
إذا كانت مغامرات تصحيح الأخطاء الخاصة بك تتطلب MethodInstance
، يمكنك البحث عنها عن طريق استدعاء Core.Compiler.specialize_method
باستخدام العديد من المتغيرات المذكورة أعلاه. يمكن الحصول على كائن CodeInfo
مع
# Returns the CodeInfo object for `convert(Int, ::UInt)`:
ci = (@code_typed convert(Int, UInt(1)))[1]
The inlining algorithm (inline_worthy
)
يعمل الكثير من أصعب المهام لتمرير الإدراج في ssa_inlining_pass!
. ومع ذلك، إذا كان سؤالك هو "لماذا لم يتم إدراج دالتي؟" فستكون مهتمًا على الأرجح بـ inline_worthy
، الذي يتخذ قرارًا بشأن إدراج استدعاء الدالة أم لا.
inline_worthy
ينفذ نموذج تكلفة، حيث يتم تضمين الوظائف "الرخيصة"؛ بشكل أكثر تحديدًا، نقوم بتضمين الوظائف إذا كانت مدة تشغيلها المتوقعة ليست كبيرة مقارنة بالوقت الذي سيستغرقه issue a call إذا لم يتم تضمينها. نموذج التكلفة بسيط للغاية ويتجاهل العديد من التفاصيل المهمة: على سبيل المثال، يتم تحليل جميع حلقات for
كما لو كانت ستنفذ مرة واحدة، وتكلفة جملة if...else...end
تشمل التكلفة المجمعة لجميع الفروع. من الجدير أيضًا الاعتراف بأننا حاليًا نفتقر إلى مجموعة من الوظائف المناسبة لاختبار مدى دقة نموذج التكلفة في التنبؤ بتكلفة وقت التشغيل الفعلية، على الرغم من أن BaseBenchmarks يوفر قدرًا كبيرًا من المعلومات غير المباشرة حول النجاحات والإخفاقات لأي تعديل على خوارزمية التضمين.
أساس نموذج التكلفة هو جدول بحث، يتم تنفيذه في add_tfunc
ومستخدميه، الذي يخصص تكلفة مقدرة (تقاس بدورات وحدة المعالجة المركزية) لكل من دوال جوليا الداخلية. تستند هذه التكاليف إلى standard ranges for common architectures (انظر Agner Fog's analysis لمزيد من التفاصيل).
نحن نكمل جدول البحث منخفض المستوى هذا بعدد من الحالات الخاصة. على سبيل المثال، يتم تعيين تعبير :invoke
(وهو استدعاء تم استنتاج جميع أنواع المدخلات والمخرجات له مسبقًا) بتكلفة ثابتة (حاليًا 20 دورة). بالمقابل، يشير تعبير :call
، بالنسبة للدوال غير الداخلية/المضمنة، إلى أن الاستدعاء سيتطلب توجيه ديناميكي، وفي هذه الحالة نعين تكلفة تحددها Params.inline_nonleaf_penalty
(المحددة حاليًا بـ 1000
). لاحظ أن هذا ليس تقديرًا "من المبادئ الأساسية" للتكلفة الخام للتوجيه الديناميكي، بل هو مجرد قاعدة إرشادية تشير إلى أن التوجيه الديناميكي مكلف للغاية.
يتم تحليل كل بيان لتكلفته الإجمالية في دالة تسمى statement_cost
. يمكنك عرض التكلفة المرتبطة بكل بيان على النحو التالي:
julia> Base.print_statement_costs(stdout, map, (typeof(sqrt), Tuple{Int},)) # map(sqrt, (2,))
map(f, t::Tuple{Any}) @ Base tuple.jl:281
0 1 ─ %1 = $(Expr(:boundscheck, true))::Bool
0 │ %2 = Base.getfield(_3, 1, %1)::Int64
1 │ %3 = Base.sitofp(Float64, %2)::Float64
0 │ %4 = Base.lt_float(%3, 0.0)::Bool
0 └── goto #3 if not %4
0 2 ─ invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %3::Float64)::Union{}
0 └── unreachable
20 3 ─ %8 = Base.Math.sqrt_llvm(%3)::Float64
0 └── goto #4
0 4 ─ goto #5
0 5 ─ %11 = Core.tuple(%8)::Tuple{Float64}
0 └── return %11
تكاليف الخطوط في العمود الأيسر. يشمل ذلك عواقب الإدراج وأشكال أخرى من التحسين.