تحسينات مترجم JIT لـ .NET 5

منذ بعض الوقت ، بدأت رحلة مدهشة إلى عالم مترجم 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 قبل التحسين الخاص بي (على سبيل المثال ، للتخصص الدولي) لجميع الطرق الثلاث:



والآن بعد التحسين:



الحقيقة هي أن التحسين وجد أنماطًا من كود IL

box !!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)
{
    // if ((uint) array.Length <= (uint) index)
    //     throw new IndexOutOfRangeException();
    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


المرحلة: Morph
C من هذه العلاقات العامة وبدأت في الغوص المذهل في عالم تحسينات JIT. عملية "القسمة" أبطأ من عملية "الضرب" (وإذا كانت للأعداد الصحيحة وبصفة عامة - ترتيب الحجم). يعمل للثوابت فقط يساوي قوة اثنين ، على سبيل المثال:

static float DivideBy2(float x) => x / 2; // = x * 0.5; 

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 قبل التحسين:



وبعد:



رقم PR 33024: x * 2 ⇨ x + x


المرحلة: خفض
أيضا الجزئي بسيطة جدا (نانو) 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 الذي يشتمل على كل ما هو ممكن ويملأ الثوابت.

All Articles