c# - المحاولة مسرعة الكود الخاص بي؟




.net clr (4)

أحد مهندسي Roslyn المتخصص في فهم تحسين استخدام المكدس أخذ نظرة على هذا الأمر وأبلغني أنه يبدو أن هناك مشكلة في التفاعل بين الطريقة التي يولّد بها المجمع C # مخازن متغيرة محلية والطريقة التي سجل بها برنامج التحويل البرمجي JIT جدولة في رمز x86 المطابق. والنتيجة هي توليد التعليمات البرمجية دون المستوى الأمثل على الأحمال والمخازن من السكان المحليين.

لسبب ما غير واضح لنا جميعا ، يتم تجنب مسار توليد التعليمات البرمجية المثير للمشاكل عندما يعرف JITter أن الكتلة في منطقة محمية بالمحاولة.

هذا غريب جدا. سنتابع مع فريق JITter ونرى ما إذا كان بإمكاننا الحصول على خطأ تم إدخاله حتى يتمكنوا من إصلاح ذلك.

أيضًا ، نحن نعمل على إدخال تحسينات على Roslyn إلى خوارزميتي C # و VB compilers لتحديد الوقت الذي يمكن فيه جعل السكان المحليين "سريعًا" - أي ، يتم دفعهم وتسجيلهم على المكدس فقط ، بدلاً من تخصيص موقع محدد على المكدس مدة التنشيط. نحن نعتقد أن JITter سوف تكون قادرة على القيام بعمل أفضل من تخصيص السجل وماذا لو أعطيناها تلميحات أفضل حول متى يمكن جعل السكان المحليين "ميتين" في وقت سابق.

شكرًا على لفت انتباهنا ، واعتذارات عن السلوك الغريب.

كتبت بعض التعليمات البرمجية لاختبار تأثير محاولة الصيد ، ولكن رؤية بعض النتائج المدهشة.

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

على جهاز الكمبيوتر الخاص بي ، يطبع هذا باستمرار قيمة حول 0.96.

عندما أقوم بحل حلقة for في داخل Fibo () مع كتلة try-catch مثل هذا:

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

الآن يطبع باستمرار 0.69 ... - أنه يعمل في الواقع أسرع! لكن لماذا؟

ملاحظة: قمت بترجمة هذا باستخدام تهيئة الإصدار وقمت بتشغيل ملف EXE مباشرة (خارج Visual Studio).

EDIT: يبين التحليل الممتاز الذي قام به جون سكيت أن تجربة المحاولة تتسبب بطريقة ما في استخدام x86 CLR لتسجيلات وحدة المعالجة المركزية بطريقة أكثر تفضيلاً في هذه الحالة المحددة (وأعتقد أننا لم نفهم بعد السبب). لقد أكدت اكتشاف جون أن x64 CLR لا يوجد لديه هذا الاختلاف ، وأنه كان أسرع من x86 CLR. لقد اختبرت أيضًا استخدام أنواع int داخل أسلوب Fibo بدلاً من أنواع long ، ومن ثم كانت x86 CLR بنفس سرعة x64 CLR.

استكمال: يبدو أن هذه المشكلة قد تم إصلاحها من قبل Roslyn. نفس الجهاز ، نفس إصدار CLR - تظل المشكلة كما هو موضح أعلاه عند تجميعها مع VS 2013 ، ولكن المشكلة تختفي عند تجميعها مع VS 2015.


تظهر تفكيك جون أن الفرق بين النسختين هو أن النسخة السريعة تستخدم زوجًا من التسجيلات ( esi,edi ) لتخزين أحد المتغيرات المحلية حيث لا يعمل الإصدار البطيء.

يقوم المترجم JIT بعمل افتراضات مختلفة فيما يتعلق باستخدام السجل للشفرة التي تحتوي على كتلة try-catch مقابل الكود الذي لا. هذا يؤدي إلى إجراء اختيارات مختلفة لتوزيع السجل. في هذه الحالة ، يفضل هذا الرمز مع كتلة try-catch. قد يؤدي رمز مختلف إلى التأثير المعاكس ، لذلك لن أحسب ذلك كتقنية تسريع للأغراض العامة.

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

على سبيل المثال ، ضع في الاعتبار الأساليب التالية اثنين. تم تكييفها من مثال واقعي:

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

واحد هو نسخة عامة من الآخر. استبدال النوع العام مع StructArray شأنه أن يجعل الطرق متطابقة. لأن StructArray هو نوع قيمة ، فإنه يحصل على الإصدار الخاص به من الطريقة العامة. ومع ذلك ، فإن وقت التشغيل الفعلي أطول بكثير من الطريقة المتخصصة ، ولكن فقط لـ x86. بالنسبة إلى x64 ، تكون التوقيتات متطابقة إلى حد كبير. في حالات أخرى ، لاحظت وجود اختلافات لـ x64 أيضًا.


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


يبدو أن حالة التضمين كانت سيئة. على النواة x86 ، يحتوي الارتعاش على ebx و edx و esi و edi على تخزين متاح للأغراض العامة للمتغيرات المحلية. يصبح سجل ecx متاحًا بطريقة ثابتة ، ولا يلزم تخزينه. مطلوب تسجيل eax غالبًا لإجراء العمليات الحسابية. ولكن هذه هي سجلات 32 بت ، للمتغيرات من نوع طويلة يجب استخدام زوج من السجلات. والتي هي edx: eax للحسابات و edi: ebx للتخزين.

وهو ما يبرز في عملية التفكيك للإصدار البطيء ، ولا يتم استخدام edi أو ebx.

عندما يتعذر على الارتعاش العثور على سجلات كافية لتخزين المتغيرات المحلية ، يجب عليه إنشاء رمز لتحميلها وتخزينها من إطار المكدس. يؤدي ذلك إلى إبطاء الشفرة ، ويمنع تحسين المعالجات المسمى "تسجيل إعادة تسمية" ، وهي خدعة تحسين أساسية للمعالج الداخلي تستخدم نسخًا متعددة من سجل وتسمح بالتنفيذ الفائق. الذي يسمح لعدة تعليمات بالتشغيل في نفس الوقت ، حتى عندما يستخدمون نفس السجل. عدم وجود ما يكفي من السجلات هو مشكلة شائعة في النوى x86 ، التي تمت معالجتها في x64 والتي تحتوي على 8 سجلات إضافية (من R9 إلى r15).

ستبذل الارتعاش قصارى جهدها لتطبيق تحسين آخر لتوليد التعليمات البرمجية ، وستحاول تضمين طريقة Fibo () الخاصة بك. بمعنى آخر ، عدم إجراء استدعاء إلى الطريقة ولكن إنشاء رمز للأسلوب المضمن في الأسلوب Main (). تحسين مهم للغاية ، لأنه ، لواحد ، يجعل خصائص فئة C # مجانية ، مما يمنحهم حقلاً من الحقل. يتجنب العبء إجراء استدعاء الأسلوب وإعداد إطار المكدس الخاص به ، يحفظ بضع nanoseconds.

هناك العديد من القواعد التي تحدد بالضبط متى يمكن تضمين طريقة ما. لم يتم توثيقها بدقة ولكن تم ذكرها في مشاركات المدونة. إحدى القواعد هي أنه لن يحدث عندما يكون جسم الطريقة كبيرًا جدًا. يؤدي ذلك إلى هزيمة المكاسب الناتجة عن التضمين ، ويولد الكثير من التعليمات البرمجية التي لا تتناسب أيضًا مع ذاكرة التخزين المؤقت للتعليمات L1. هناك قاعدة أخرى صارمة تنطبق هنا وهي أن الطريقة لن تكون مائلة عندما تحتوي على عبارة try / catch. والخلفية وراء ذلك واحدة هي تفاصيل تنفيذ الاستثناءات ، حيث تعود إلى دعم Windows المدمج (SEH) (هيكل معالجة الاستثناءات) الذي يعتمد على إطار المكدس.

يمكن استنتاج سلوك واحد من خوارزمية تخصيص السجل في الارتعاش من اللعب بهذا الرمز. يبدو أن على علم عندما يحاول الارتعاش مضمنة طريقة. إحدى القواعد التي يبدو أنها تستخدم ذلك edx فقط: يمكن استخدام زوج تسجيل eax لرمز مضمن يحتوي على متغيرات محلية من النوع الطويل. لكن لا edi: ebx. لا شك في أن ذلك سيكون ضارًا جدًا لتوليد التعليمات البرمجية لطريقة الاستدعاء ، حيث يعد كل من edi و ebx بمثابة سجلات تخزين مهمة.

حتى تحصل على نسخة سريعة لأن غضب يعلم في خط الهجوم أن الجسم الأسلوب يحتوي على عبارات محاولة / الصيد. يعرف أنه لا يمكن أن يكون مضمنًا بحيث يستخدم بسهولة edi: ebx للتخزين للمتغير الطويل. لقد حصلت على الإصدار البطيء نظرًا لأن الارتعاش لم يكن على علم بالأمر ، حيث لا يعمل الخط الداخلي. وجدت فقط بعد إنشاء رمز للجسم الأسلوب.

العيب إذن هو أنها لم تعد وتعيد إنشاء الشفرة الخاصة بهذه الطريقة. وهو أمر مفهوم ، نظرًا لضيق الوقت الذي يتعين عليه العمل فيه.

لا يحدث هذا بطء على x64 لأنه لدى أحد 8 تسجيلات أكثر. لآخر لأنه يمكن تخزين طويلة في سجل واحد فقط (مثل راكس). والبطء لا يحدث عند استخدام كثافة العمليات بدلا من فترة طويلة لأن الارتعاش لديه الكثير من المرونة في اختيار السجلات.







performance-testing