مقالة غير ناجحة حول تسريع التفكير

سأشرح على الفور عنوان المقال. كانت الخطة الأصلية هي تقديم نصيحة جيدة وموثوقة حول تسريع استخدام الانعكاس باستخدام مثال بسيط ولكنه واقعي، ولكن أثناء المقارنة المرجعية اتضح أن الانعكاس ليس بطيئًا كما اعتقدت، LINQ أبطأ مما كان عليه في كوابيسي. لكن في النهاية اتضح أنني أيضًا أخطأت في القياسات... تفاصيل قصة الحياة هذه تحت المقطع وفي التعليقات. نظرًا لأن المثال شائع جدًا ويتم تنفيذه من حيث المبدأ كما يحدث عادةً في المؤسسة، فقد اتضح أنه مثير للاهتمام تمامًا، كما يبدو لي، عرضًا للحياة: كان التأثير على سرعة الموضوع الرئيسي للمقال غير ملحوظ بسبب المنطق الخارجي: Moq، Autofac، EF Core وغيرها من "النطاقات".

بدأت العمل تحت انطباع هذا المقال: لماذا يكون الانعكاس بطيئا؟

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

وبالنظر إلى أنني كنت دائمًا أحمل رأيًا مماثلاً حول سرعة التفكير، لم أكن أنوي بشكل خاص التشكيك في استنتاجات المؤلف.

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

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

أعتقد أن الكثير منكم، بعد قراءة ريختر أو غيره من الأيديولوجيين، صادفوا بيانًا عادلاً تمامًا مفاده أن الانعكاس في الكود هو ظاهرة لها تأثير سلبي للغاية على أداء التطبيق.

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

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

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

كيف يتم تحقيق ذلك؟ في الأساس، لا يختلف هذا عن المنطق الذي تستخدمه المنصة نفسها لإنشاء كود JIT. عندما يتم استدعاء الطريقة لأول مرة، يتم تجميعها (ونعم، هذه العملية ليست سريعة)؛ في الاستدعاءات اللاحقة، يتم نقل التحكم إلى الطريقة المترجمة بالفعل، ولن يكون هناك أي تراجع كبير في الأداء.

في حالتنا، يمكنك أيضًا استخدام التحويل البرمجي JIT ثم استخدام السلوك المترجم بنفس الأداء الذي تتمتع به نظيراتها من AOT. سوف تأتي التعبيرات لمساعدتنا في هذه الحالة.

ويمكن صياغة المبدأ المطروح باختصار على النحو التالي:
يجب عليك تخزين النتيجة النهائية للانعكاس مؤقتًا كمفوض يحتوي على الوظيفة المترجمة. من المنطقي أيضًا تخزين كافة الكائنات الضرورية مع معلومات النوع في الحقول الخاصة بنوعك، العامل، المخزنة خارج الكائنات.

هناك منطق في هذا. يخبرنا الفطرة السليمة أنه إذا كان من الممكن تجميع شيء ما وتخزينه مؤقتًا، فيجب القيام بذلك.

بالنظر إلى المستقبل، يجب أن يقال أن ذاكرة التخزين المؤقت في العمل مع الانعكاس لها مزاياها، حتى لو كنت لا تستخدم الطريقة المقترحة لتجميع التعبيرات. في الواقع، أنا هنا أكرر ببساطة أطروحات كاتب المقال الذي أشير إليه أعلاه.

الآن عن الكود. دعونا نلقي نظرة على مثال يستند إلى الألم الذي تعرضت له مؤخرًا والذي كان علي مواجهته أثناء الإنتاج الجاد لمؤسسة ائتمانية جادة. جميع الكيانات وهمية حتى لا يخمنها أحد.

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

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

نحن ننفذ وننشئ الاختبارات. يعمل.

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

المنطق كما يلي: يتلقى أسلوب القالب أزواجًا تم إنشاؤها بواسطة منطق المحلل اللغوي الأساسي. طبقة LINQ هي المحلل اللغوي والمنطق الأساسي للمرطب، الذي يقدم طلبًا إلى سياق قاعدة البيانات ويقارن المفاتيح بأزواج من المحلل اللغوي (لهذه الوظائف يوجد كود بدون LINQ للمقارنة). بعد ذلك، يتم تمرير الأزواج إلى طريقة الترطيب الرئيسية ويتم تعيين قيم الأزواج على الخصائص المقابلة للكيان.

"سريع" (بادئة سريعة في المعايير):

 protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var setterMapItem in _proprtySettersMap)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                setterMapItem.Value(contact, correlation?.Value);
            }
            return contact;
        }

كما نرى، يتم استخدام مجموعة ثابتة مع خصائص الضبط - لامدا المترجمة التي تستدعي كيان الضبط. تم إنشاؤها بواسطة الكود التالي:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action<Contact, string> GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();
            
            var lambda = (Expression<Action<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

بشكل عام فمن الواضح. نحن نجتاز الخصائص، وننشئ مندوبين لهم يقومون باستدعاء المستوطنين، ونحفظهم. ثم نتصل عند الضرورة.

"بطيء" (بادئة بطيئة في المعايير):

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var property in _properties)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                if (correlation?.Value == null)
                    continue;

                property.SetValue(contact, correlation.Value);
            }
            return contact;
        }

نحن هنا نتجاوز الخصائص على الفور ونستدعي SetValue مباشرة.

من أجل الوضوح وكمرجع، قمت بتطبيق طريقة ساذجة تكتب قيم أزواج الارتباط الخاصة بها مباشرة في حقول الكيان. البادئة - دليل.

الآن لنأخذ BenchmarkDotNet ونفحص الأداء. وفجأة... (حرق - هذه ليست النتيجة الصحيحة، التفاصيل أدناه)

مقالة غير ناجحة حول تسريع التفكير

ما الذي نراه هنا؟ الأساليب التي تحمل البادئة السريعة منتصرة تبين أنها أبطأ في جميع التمريرات تقريبًا من الأساليب التي تحمل البادئة البطيئة. وهذا ينطبق على كل من التخصيص وسرعة العمل. من ناحية أخرى، فإن التنفيذ الجميل والأنيق لرسم الخرائط باستخدام أساليب LINQ المخصصة لهذا حيثما كان ذلك ممكنًا، على العكس من ذلك، يقلل الإنتاجية بشكل كبير. الفرق هو النظام. لا يتغير الاتجاه مع أعداد مختلفة من التمريرات. والفرق الوحيد هو في الحجم. مع LINQ يكون الأمر أبطأ بمقدار 4 إلى 200 مرة، وهناك المزيد من القمامة على نفس المقياس تقريبًا.

تحديث

لم أصدق عيني، لكن الأهم من ذلك أن زميلنا لم يصدق عيني ولا شفرتي - ديمتري تيخونوف 0x1000000. بعد التحقق مرة أخرى من الحل الذي قدمته، اكتشف ببراعة وأشار إلى خطأ فاتني بسبب عدد من التغييرات في التنفيذ، من الأولي إلى النهائي. بعد إصلاح الخلل الذي تم العثور عليه في إعداد Moq، أصبحت جميع النتائج في مكانها الصحيح. وفقًا لنتائج إعادة الاختبار، لم يتغير الاتجاه الرئيسي - لا يزال LINQ يؤثر على الأداء أكثر من الانعكاس. ومع ذلك، فمن الجيد أن العمل مع تجميع التعبير لم يتم عبثا، والنتيجة مرئية في وقت التخصيص والتنفيذ. الإطلاق الأول، عند تهيئة الحقول الثابتة، يكون أبطأ بشكل طبيعي بالنسبة للطريقة "السريعة"، ولكن بعد ذلك يتغير الوضع.

وهنا نتيجة إعادة الاختبار:

مقالة غير ناجحة حول تسريع التفكير

الخلاصة: عند استخدام الانعكاس في المؤسسة، ليست هناك حاجة خاصة للجوء إلى الحيل - سوف يأكل LINQ الإنتاجية أكثر. ومع ذلك، في الأساليب عالية التحميل التي تتطلب التحسين، يمكنك حفظ الانعكاس في شكل مُهيئات ومترجمين مندوبين، والذي سيوفر بعد ذلك منطقًا "سريعًا". بهذه الطريقة يمكنك الحفاظ على مرونة الانعكاس وسرعة التطبيق.

الرمز القياسي متاح هنا. يمكن لأي شخص أن يتأكد من كلامي:
اختبارات انعكاس هابرا

ملاحظة: يستخدم الكود في الاختبارات IoC، وفي المعايير يستخدم بنية واضحة. الحقيقة هي أنني في التنفيذ النهائي قمت بقطع جميع العوامل التي قد تؤثر على الأداء وتجعل النتيجة صاخبة.

PPS: شكرا للمستخدم ديمتري تيخونوف @0x1000000 لاكتشاف خطأي في إعداد موك، والذي أثر على القياسات الأولى. إذا كان لدى أي من القراء الكارما الكافية، يرجى الإعجاب بها. توقف الرجل، وقرأ الرجل، ودقق الرجل مرتين وأشار إلى الخطأ. أعتقد أن هذا يستحق الاحترام والتعاطف.

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

المصدر: www.habr.com

إضافة تعليق