مقالة فاشلة حول تسريع التفكير

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

لقد بدأت عملي تحت انطباع هذه المقالة: لماذا بطيء التفكير

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

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

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

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

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

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

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

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

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

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

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

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

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

الآن عن الرمز. دعونا نلقي نظرة على مثال قائم على ألمي الأخير الذي كان علي مواجهته في الإنتاج الجاد لمنظمة ائتمانية جادة. جميع الكيانات وهمية حتى لا يخمن أحد.

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

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

نقوم بتنفيذ وإنشاء الاختبارات. يعمل.

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

المنطق هو كما يلي: أسلوب القالب يتلقى الأزواج التي شكلها منطق المحلل اللغوي الأساسي. إن مستوى LINQ هو المحلل اللغوي والمنطق الأساسي للهيدروليكي ، حيث يقدم طلبًا إلى سياق db ويطابق المفاتيح مع أزواج من المحلل اللغوي (لهذه الوظائف ، يوجد رمز بدون 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;
        }

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

        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 وندرس الإنتاجية. وفجأة ... (المفسد ليس هو النتيجة الصحيحة ، التفاصيل أدناه)



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

محدث

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

هنا نتيجة الاختبار:



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

يتوفر رمز بمعيار هنا. يمكن للجميع التحقق مرة أخرى من كلماتي:
HabraReflectionTests

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

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

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

All Articles