كيف يدمج JIT كود C # الخاص بنا (الاستدلال)

التضمين هو أحد أهم التحسينات في المترجمات. إنه لا يزيل الحمل الزائد من المكالمة فحسب ، بل يفتح أيضًا العديد من الاحتمالات للتحسينات الأخرى ، على سبيل المثال ، الطي المستمر ، وإزالة الرمز الميت ، وما إلى ذلك. علاوة على ذلك ، يؤدي التضمين أحيانًا إلى انخفاض في حجم وظيفة الاستدعاء! سألت عدة أشخاص عما إذا كانوا يعرفون ما هي القواعد التي يتم تضمينها في الدالات الموجودة في C # ، وأجاب معظمهم بأن JIT ينظر إلى حجم رمز IL ويضمن فقط الوظائف الصغيرة بحجم يصل إلى 32 بايت على سبيل المثال. لذلك ، قررت كتابة هذا المنشور للكشف عن تفاصيل التنفيذ بمساعدة مثل هذا المثال ، والذي سيظهر العديد من الاستدلال في العمل في وقت واحد:




هل تعتقد أن المكالمة إلى مُنشئ الحجم سيتم تضمينها هنا؟ من الواضح أنه لا. إنه كبير جدًا ، خاصةً بسبب الوزن الثقيل الذي يلقي المشغلين الجدد ، مما يؤدي إلى تشفير جريء إلى حد ما. دعونا تحقق في Disasmo:



مضمنة! علاوة على ذلك ، تم حذف جميع الاستثناءات وفروعها بنجاح! يمكنك أن تقول شيئًا بأسلوب "آه ، حسنًا ، جيت ذكي للغاية وقام بتحليل كامل لجميع المرشحين للتضمين ، ونظر في ما سيحدث إذا اجتازت حجج معينة" أو "يحاول jit تضمين كل ما هو ممكن ، ويؤدي كل التحسينات ، ثم يقرر بشكل مربح أم لا "(التقط التوافقات وحساب تعقيد هذه العملية ، على سبيل المثال ، لرسم بياني استدعاء لعشر أو طريقتين).

حسنًا ... لا ، هذا غير واقعي ، خاصة من حيث الوقت المناسب. لذلك ، يستخدم معظم المترجمين ما يسمى الملاحظات والاستدلال لحل مشكلة حقيبة الظهر الكلاسيكية هذه ومحاولة تحديد ميزانيتهم ​​الخاصة وتناسبها بأكبر قدر ممكن من الكفاءة (ولا ، PGO ليست دواء لكل داء). لدى RyuJIT ملاحظات إيجابية وسلبية. زيادة معامل الاستحقاق (مضاعف المنافع). كلما زاد المعامل ، زاد عدد الرموز التي يمكننا تضمينها. الملاحظات السلبية على العكس - خفضه أو حتى حظر التضمين. دعونا نرى ما هي الملاحظات التي قام بها RyuJIT لمثالنا:



يمكن مشاهدة هذه الملاحظات في السجلات من COMPlus_JitDump (على سبيل المثال ، في Disasmo):



كل هذه الملاحظات البسيطة زادت المعامل من 1.0إلى 11.5 وساعد في التغلب على ميزانية المضمن ، على سبيل المثال ، حقيقة أننا نجتاز حجة مستمرة ومقارنتها مع ثابت آخر يخبرنا أنه مع درجة عالية من الاحتمال بعد انهيار الثوابت ، سيتم حذف أحد الفروع الشرطية وسيصبح الرمز أصغر. أو ، على سبيل المثال ، حقيقة أن هذا هو منشئ ويسمى داخل الحلقة هو أيضا تلميح إلى jit أنه يجب أن يخفف متطلبات التضمين.

بالإضافة إلى مضاعف الفائدة ، يستخدم RyuJIT أيضًا الملاحظات للتنبؤ بحجم رمز الوظيفة الأصلية وتأثير أدائه باستخدام الثوابت السحرية في EstimateCodeSize () و EstimatePerformanceImpact () التي تم الحصول عليها باستخدام ML.

بالمناسبة ، هل لاحظت هذه الحيلة؟:

if ((value - 'A') > ('Z' - 'A'))

هذه نسخة محسنة من أجل:

if (value < 'A' || value > 'Z')

كلا التعبيرين متماثلان ، ولكن في الحالة الأولى لدينا وحدة قاعدة واحدة ، وفي الثانية هناك ثلاثة منها. اتضح أن Inliner لديها حد صارم على عدد الكتل الأساسية في الوظيفة وإذا تجاوزت 5 ، فلا يهم حجم مضاعف مصلحتنا - يتم إلغاء التضمين. لذلك قمت بتطبيق هذه الخدعة لتناسب هذا المطلب الصارم. سيكون من الرائع إذا فعلت روسلين ذلك من أجلي.

المسألة في روزلين : github.com/dotnet/runtime/issues/13347
العلاقات العامة في RyuJIT (محاولتي حرج): github.com/dotnet/coreclr/pull/27480

هناك وصفت مثال لماذا كان من المنطقي أن تفعل ليس فقط في جيت ولكن وفي المترجم C #.

الأساليب المضمنة والافتراضية


كل شيء واضح هنا ، لا يمكنك تضمين ما لا توجد معلومات عنه في مرحلة التجميع ، على الرغم من أن النوع أو الطريقة مختومة ، فلماذا لا .

ضمانة ورمي استثناءات


إذا كانت إحدى الطرق لا تُرجع أبدًا قيمة (على سبيل المثال ، فقط throw new...) ، فعندئذٍ يتم وضع علامة على هذه الأساليب تلقائيًا على أنها أدوات مساعدة في الرمي ولن يتم تضمينها. هذه طريقة لمسح الكود المعقد من throw newتحت السجادة واسترضاء البطانة.

السمة الضمنية [AggressiveInlining]


في هذه الحالة ، تنصح بأن يكون الخط مضمنًا في الطريقة ، ولكن يجب أن تكون شديد الحذر لسببين:

  • ربما تقوم بتحسين حالة واحدة وتفاقم كل الحالات الأخرى (على سبيل المثال ، تحسين حالة الحجج الثابتة) حسب حجم الكود.
  • غالبًا ما يولد التضمين عددًا كبيرًا من المتغيرات المؤقتة التي يمكن أن تتجاوز حدًا معينًا - عدد المتغيرات التي يمكن أن تتتبع دورة حياتها RyuJIT (512) وبعد ذلك سيبدأ الرمز في النمو إلى انسكابات رهيبة على المكدس ويتباطأ بشكل كبير. مثالان جيدان : tyts و tyts .

الأساليب المبطنة والديناميكية


في الوقت الحالي ، لا يتم تضمين هذه الأساليب ولا يتم تضمينها في نفسها: github.com/dotnet/runtime/issues/34500

محاولتي لكتابة ارشادي


في الآونة الأخيرة ، حاولت كتابة الاستدلال الخاص بك للمساعدة هنا مثل هذه المناسبة:



في آخر مشاركة ذكرت أنني قمت بتحسين حساب RyuJIT لطول السلاسل الثابتة ( "Hello".Length -> 5نرى أنه إذا كانت zainlaynit) ، وهكذا ، في المثال أعلاه ^ Validateفي Test، نحصل if ("hello".Length > 10)ما هو الأمثل في if (5 > 10)ما هو الأمثل في إزالة الشرط / الفرع بأكمله. ومع ذلك ، رفضت شركة Inliner التضمين Validate:



والمشكلة الرئيسية هنا هي أنه لا يوجد دليل استرشادي حتى الآن يخبر Jit أننا نمرر سلسلة ثابتة إليه System.String::get_Length، مما يعني أن Callvirt-call سوف تنهار على الأرجح إلى ثابت وسيتم حذف الفرع بأكمله. في الواقع ، ارشاديويضيف هذه الملاحظة (ناقص الوحيد هو أنه يجب عليك حل جميع callvirts ، وهو ليس سريعًا جدًا).

هناك قيود أخرى ، يمكن العثور على قائمة بها بشكل عام هنا . وهنا يمكنك أن تقرأ الأفكار واحد من المطورين JIT الرئيسي حول تصميم inliner و مقالته على استخدام آلة التعلم لهذه القضية.

All Articles