منذ بعض الوقت ، بدأت رحلة مدهشة إلى عالم مترجم JIT من أجل العثور على أماكن يمكنك فيها وضع يديك وتسريع شيء ما ، كما في سياق العمل الرئيسي ، تراكمت كمية صغيرة من المعرفة في LLVM وتحسيناتها. في هذه المقالة ، أود أن أشارك قائمة بالتحسينات التي أدخلتها على JIT (في .NET. يطلق عليه RyuJIT تكريمًا لبعض التنين أو الأنمي - لم أتمكن من معرفة ذلك) ، وقد وصل معظمها إلى سيد بالفعل وسيكون متاحًا في .NET (Core) 5 تؤثر التحسينات التي أجريتها على مراحل مختلفة من JIT ، والتي يمكن عرضها بشكل تخطيطي للغاية على النحو التالي:
كما هو واضح من الرسم البياني ، JIT هي وحدة منفصلة تتعلق بواجهة Jit الضيقة ، والتي يمكن من خلالها استشارة JIT في بعض الأشياء ، على سبيل المثال ، هل من الممكنيلقي فئة إلى أخرى. كلما جمعت JIT في وقت لاحق الطريقة في Tier1 ، زادت المعلومات التي يمكن أن يوفرها وقت التشغيل ، على سبيل المثال ، أنه static readonly
يمكن استبدال الحقول بثابت ، لأن تم تهيئة الصف بالفعل بشكل ثابت.لذا ، لنبدأ بالقائمة.PR # 1817 : تحسينات الملاكمة وفتحها في مطابقة الأنماط
المرحلة: المستوردكثير من ميزات C # الجديدة غالبًا ما تخطئ عن طريق إدخال رموز opil box / unbox . هذه عملية مكلفة للغاية ، وهي في الأساس تخصيص كائن جديد في كومة الذاكرة المؤقتة ، ونسخ القيمة من المكدس إليها ، ثم تحميل GC في النهاية. يوجد بالفعل عدد من التحسينات في JIT لهذه الحالة ، ولكني وجدت تطابق نمط مفقود في C # 8 ، على سبيل المثال:public static int Case1<T>(T o)
{
if (o is int x)
return x;
return 0;
}
public static int Case2<T>(T o) => o is int n ? n : 42;
public static int Case3<T>(T o)
{
return o switch
{
int n => n,
string str => str.Length,
_ => 0
};
}
ودعنا نرى كود asm قبل التحسين الخاص بي (على سبيل المثال ، للتخصص الدولي) لجميع الطرق الثلاث:
والآن بعد التحسين:
الحقيقة هي أن التحسين وجد أنماطًا من كود ILbox !!T
isinst Type1
unbox.any Type2
عند الاستيراد والحصول على معلومات حول الأنواع ، تمكنت ببساطة من تجاهل رموز opcodes هذه وعدم إدراج boxing-anboxing. بالمناسبة ، قمت بتنفيذ نفس التحسين في مونو أيضًا. فيما يلي ، يوجد رابط إلى طلب السحب في رأس وصف التحسين.PR # 1157 typeof (T) .IsValueType ⇨ صواب / خطأ
المرحلة: المستوردهنا ، قمت بتدريب JIT على استبدال Type.IsValueType على الفور بثابت إن أمكن. هذا ناقص التحدي والقدرة على قطع الشروط والفروع بأكملها في المستقبل ، مثال:void Foo<T>()
{
if (!typeof(T).IsValueType)
Console.WriteLine("not a valuetype");
}
ودعنا نرى الكود الخاص بتخصص Fint <int> قبل التحسين:
وبعد التحسين:
يمكن عمل نفس الشيء مع خصائص النوع الأخرى إذا لزم الأمر.رقم العلاقات العامة 1157 typeof(T1).IsAssignableFrom(typeof(T2)) ⇨ true/false
المرحلة: المستوردنفس الشيء تقريبًا - الآن يمكنك التحقق من التسلسل الهرمي في الأساليب العامة دون خوف من عدم تحسين ذلك ، على سبيل المثال:void Foo<T1, T2>()
{
if (!typeof(T1).IsAssignableFrom(typeof(T2)))
Console.WriteLine("T1 is not assignable from T2");
}
بنفس الطريقة ، سيتم استبداله بثابت true/false
ويمكن حذف الشرط بالكامل. في مثل هذه التحسينات ، بالطبع ، هناك بعض الحالات الزاوية التي يجب أن تضعها في اعتبارك دائمًا: النظام. _الأدوية المشتركة من Canon ، والمصفوفات ، ومتغير co (ntr) ، والقيم الفارغة ، وكائنات COM ، إلخ.رقم العلاقات العامة 1378 "Hello".Length ⇨ 5
المرحلة: المستورد علىالرغم من حقيقة أن التحسين واضح وبسيط قدر الإمكان ، كان علي أن أتعرق كثيرًا لتطبيقه في JIT-e. الشيء هو أن JIT لم يكن يعرف عن محتويات السلسلة ، فقد رأى حرفية السلسلة ( GT_CNS_STR ) ، لكنه لم يعرف أي شيء عن المحتويات المحددة للسلاسل. كان علي مساعدته عن طريق الاتصال بـ VM (لتوسيع واجهة JIT- المذكورة أعلاه) ، والتحسين نفسه هو في الأساس بضعة أسطر من التعليمات البرمجية . هناك الكثير من حالات المستخدم ، إلى جانب الحالات الواضحة ، مثل: str.IndexOf("foo") + "foo".Length
إلى الحالات غير الواضحة التي يتضمنها التضمين (أذكرك: Roslyn لا تتعامل مع التضمين ، لذلك سيكون هذا التحسين غير فعال في ذلك ، بالإضافة إلى جميع الحالات الأخرى) ، على سبيل المثال:bool Validate(string str) => str.Length > 0 && str.Length <= 100;
bool Test() => Validate("Hello");
دعونا ننظر في codegen ل اختبار ( التحقق من صحة غير مضمنة):
والآن codegen بعد إضافة الأمثل:
أي قم بتضمين الطريقة ، واستبدل المتغيرات بالحرف الوتري ، واستبدل .الطول من الحرفي بأطوال السلسلة الحقيقية ، وطي الثوابت ، وحذف الشفرة الميتة. بالمناسبة ، بما أن JIT يمكنها الآن التحقق من محتويات السلسلة ، فتحت الأبواب للتحسينات الأخرى المتعلقة بالحرف السلسلة. تم ذكر التحسين نفسه في الإعلان عن المعاينة الأولى لـ .NET 5.0: devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-1 في قسم تحسينات جودة الرمز في RyuJIT .PR # 1644: تحسين الشيكات المقيدة.
المرحلة: إزالة التحقق من الحدودبالنسبة للكثيرين ، لن يكون سرًا أنه في كل مرة تدخل فيها إلى مصفوفة حسب الفهرس ، تقوم JIT بإدراج تحقق لك بأن الصفيف لا يتجاوز ويطرح استثناء إذا حدث ذلك - في حالة المنطق الخاطئ ، لا يمكنك لقراءة الذاكرة العشوائية ، والحصول على بعض القيمة والمتابعة.int Foo(int[] array, int index)
{
return array[index];
}
يُعد هذا الفحص مفيدًا ، ولكن يمكن أن يؤثر بشكل كبير على الأداء: أولاً ، يضيف عملية مقارنة ويجعل الشفرة الخاصة بك غير متفرعة ، وثانيًا ، يضيف رمز استدعاء استثنائي لطريقتك مع جميع العواقب. ومع ذلك ، في كثير من الحالات ، يمكن أن تقوم JIT بإزالة عمليات التحقق هذه إذا كان بإمكانها أن تثبت لنفسها أن الفهرس لن يتجاوز ذلك أبدًا ، أو أن هناك بالفعل بعض عمليات التحقق الأخرى ولا تحتاج إلى إضافة فحص آخر - إلغاء التحقق من الحدود (المدى). لقد وجدت العديد من الحالات التي لم يستطع فيها مواجهتها وتصحيحها (وفي المستقبل أخطط لمزيد من التحسينات في هذه المرحلة).var item = array[index & mask];
هنا في هذا الرمز ، أخبر JIT أن هذا & mask
يحد بشكل أساسي من الفهرس من الأعلى إلى قيمة mask
، أي إذا كانت قيمة mask
المصفوفة وطولها معروفة لـ JIT ، فلا يمكنك إدراج تدقيق مقيد. ينطبق الشيء نفسه على عمليات٪ ، (& x >> y). مثال على استخدام هذا التحسين في aspnetcore .أيضًا ، إذا علمنا أنه في صفيفنا ، على سبيل المثال ، هناك 256 عنصرًا أو أكثر ، فعندئذ إذا كان مفهرسنا غير المعروف من نوع البايت ، بغض النظر عن مدى صعوبة ذلك ، فلن يكون قادرًا على الخروج من الحدود. PR: github.com/dotnet/coreclr/pull/25912رقم العلاقات العامة 24584: x / 2 ⇨ x * 0.5
المرحلة: MorphC من هذه العلاقات العامة وبدأت في الغوص المذهل في عالم تحسينات JIT. عملية "القسمة" أبطأ من عملية "الضرب" (وإذا كانت للأعداد الصحيحة وبصفة عامة - ترتيب الحجم). يعمل للثوابت فقط يساوي قوة اثنين ، على سبيل المثال:static float DivideBy2(float x) => x / 2;
Codegen قبل التحسين:
وبعد:
إذا قارنا بين هذين التوجيهين لـ Haswell ، فسيكون كل شيء واضحًا:vdivss (Latency: 10-20, R.Throughput: 7-14)
vmulss (Latency: 5, R.Throughput: 0.5)
ويلي ذلك تحسينات لا تزال في مرحلة مراجعة الكود وليس حقيقة قبولها.رقم العلاقات العامة 31978: Math.Pow(x, 2) ⇨ x * x
المرحلة: المستوردكل شيء بسيط هنا: بدلاً من استدعاء pow (f) لحالة شائعة إلى حد ما ، عندما تكون الدرجة ثابتة 2 (حسنًا ، إنها مجانية أيضًا لـ 1 ، -1 ، 0) ، يمكنك توسيعها إلى x * x بسيطة. يمكنك توسيع أي درجات أخرى ، ولكن عليك الانتظار حتى يتم تنفيذ وضع "الرياضيات السريعة" في .NET ، حيث يمكن إهمال مواصفات IEEE-754 من أجل الأداء. مثال:static float Pow2(float x) => MathF.Pow(x, 2);
Codegen قبل التحسين:
وبعد:
المرحلة: خفضأيضا الجزئي بسيطة جدا (نانو) piphol الأمثل، ويسمح لك لضرب من قبل 2 دون تحميل ثابت في السجل.static float MultiplyBy2(float x) => x * 2;
Codegen قبل التحسين:
بعد:
بشكل عام ، التعليمات هي mul(ss/sd/ps/pd)
نفسها في زمن الانتقال والإنتاج add(ss/sd/ps/pd)
، ولكن الحاجة إلى تحميل ثابت "2" يمكن أن تبطئ العمل قليلاً. هنا ، في مثال الشفرة أعلاه ، قمت vaddss
بكل شيء في إطار تسجيل واحد.PR # 32368: تحسين الصفيف الطول / c (أو٪ s)
المرحلة: Morphحدث للتو أن حقل الطول من Array هو نوع موقّع ، والقسمة والباقي بثابت أكثر كفاءة من نوع غير موقّع (وليس فقط قوة من اثنين) ، فقط قارن هذا الكود:
My PR فقط يذكّر JIT أن Array.Length
على الرغم من أهميته ، ولكن في الواقع ، يمكن أن يكون طول المصفوفة NEVER ( ما لم تكن فوضويًا ) أقل من الصفر ، مما يعني أنه يمكنك النظر إليه كرقم غير موقع وتطبيق بعض التحسينات مثل uint.PR # 32716: الاستفادة المثلى من المقارنات البسيطة في الكود بدون فروع
المرحلة: تحليل التدفقهذه فئة أخرى من التحسينات تعمل مع الكتل الأساسية بدلاً من التعبيرات داخل واحدة. هنا JIT محافظة قليلاً ولديها مجال للتحسينات ، على سبيل المثال إدخال cmove حيثما أمكن ذلك. لقد بدأت بتحسين بسيط لهذه الحالة:x = condition ? A : B;
إذا كانت A و B ثوابتين والفرق بينهما هو الوحدة ، على سبيل المثال ، condition ? 1 : 2
عندئذٍ ، مع العلم أن عملية المقارنة في حد ذاتها تُرجع 0 أو 1 ، يمكننا استبدال القفزة بإضافة. من حيث RyuJIT ، يبدو الأمر مثل هذا:
أوصي بمشاهدة وصف العلاقات العامة نفسها ، وآمل أن يتم وصف كل شيء بوضوح هناك.ليست كل التحسينات مفيدة بنفس القدر.
تتطلب التحسينات رسومًا عالية إلى حد ما:* زيادة = تعقيد التعليمات البرمجية الحالية للدعم والقراءة* الأخطاء المحتملة: اختبار تحسينات المترجم أمر صعب للغاية ويسهل تفويت شيء والحصول على نوع من التقصير من المستخدمين.* التجميع البطيء* زيادة حجم ثنائي JITكما فهمت بالفعل ، لا يتم قبول جميع الأفكار والنماذج الأولية للتحسينات ومن الضروري إثبات أن لهم الحق في الحياة. إحدى الطرق المقبولة لإثبات ذلك في .NET هي تشغيل الأداة المساعدة jit-utils ، والتي ستقوم AOT بتجميع مجموعة من المكتبات (جميع BCL و corelib) ومقارنة كود التجميع لجميع الطرق قبل وبعد التحسينات ، وهذه هي الطريقة التي يبحث بها هذا التقرير عن التحسين"str".Length
. بالإضافة إلى التقرير ، لا تزال هناك دائرة معينة من الأشخاص (مثل jkotas ) يمكنهم ، في لمحة ، تقييم مدى فائدة واختراق كل شيء من ذروة تجربتهم وفهم أي الأماكن في .NET يمكن أن تكون عنق زجاجة وأيها لا يمكن. وشيء آخر: لا تحكم على التحسين من خلال الحجة "لا أحد يكتب" ، "سيكون من الأفضل إظهار التحذير في روسلين" - فأنت لا تعرف أبدًا كيف ستعتني شفرتك بعد JIT الذي يشتمل على كل ما هو ممكن ويملأ الثوابت.