مقاله ناموفق در مورد تسریع بازتاب

من بلافاصله عنوان مقاله را توضیح خواهم داد. طرح اولیه این بود که توصیه های خوب و قابل اعتمادی در مورد چگونگی سرعت بخشیدن به استفاده از انعکاس با استفاده از یک مثال ساده اما واقع بینانه ارائه شود، اما در طول محک زدن مشخص شد که بازتاب آنطور که فکر می کردم کند نیست، LINQ کندتر از کابوس های من است. اما در نهایت معلوم شد که من هم در اندازه گیری ها اشتباه کردم ... جزئیات این داستان زندگی در زیر برش و در نظرات. از آنجایی که مثال کاملاً رایج است و در اصل همانطور که معمولاً در یک شرکت انجام می شود اجرا می شود ، همانطور که به نظر من به نظر می رسد یک نمایش زندگی بسیار جالب بود: تأثیر بر سرعت موضوع اصلی مقاله بود. به دلیل منطق خارجی قابل توجه نیست: Moq، Autofac، EF Core و سایر "تسمه".

من تحت تأثیر این مقاله شروع به کار کردم: چرا انعکاس کند است

همانطور که می بینید، نویسنده پیشنهاد می کند به جای فراخوانی مستقیم روش های نوع بازتاب، از نمایندگان کامپایل شده به عنوان یک راه عالی برای سرعت بخشیدن به برنامه استفاده کنید. البته انتشار IL وجود دارد، اما من می خواهم از آن اجتناب کنم، زیرا این کار فشرده ترین روش برای انجام کار است که مملو از خطا است.

با توجه به اینکه من همیشه نظر مشابهی در مورد سرعت انعکاس داشته ام، به ویژه قصد نداشتم نتیجه گیری نویسنده را زیر سوال ببرم.

من اغلب با استفاده ساده لوحانه از بازتاب در شرکت مواجه می شوم. نوع گرفته شده است. اطلاعات مربوط به ملک گرفته شده است. متد SetValue فراخوانی می شود و همه خوشحال می شوند. ارزش به میدان هدف رسیده است، همه خوشحال هستند. افراد بسیار باهوش - سالمندان و رهبران تیم - برنامه‌های افزودنی خود را بر اساس چنین پیاده‌سازی ساده‌دلانه‌ای، نگاشت‌های «جهانی» از یک نوع به دیگری می‌نویسند. ماهیت معمولاً این است: همه فیلدها را می گیریم، همه ویژگی ها را می گیریم، روی آنها تکرار می کنیم: اگر نام اعضای نوع مطابقت داشته باشد، SetValue را اجرا می کنیم. هر از گاهی به دلیل اشتباهاتی که برخی از ویژگی ها را در یکی از انواع پیدا نکردیم استثناهایی را می گیریم، اما حتی در اینجا نیز راهی وجود دارد که عملکرد را بهبود می بخشد. امتحان/گرفتن

من افرادی را دیده ام که تجزیه کننده ها و نقشه برداران را مجدداً اختراع می کنند بدون اینکه کاملاً به اطلاعاتی در مورد نحوه عملکرد ماشین های قبل از آنها مجهز باشند. من دیده‌ام که مردم پیاده‌سازی‌های ساده‌لوحانه‌شان را پشت استراتژی‌ها، پشت رابط‌ها، پشت تزریق‌ها پنهان می‌کنند، به‌گونه‌ای که این امر باعث بهانه‌گیری باکانالیای بعدی می‌شود. با چنین درک هایی دماغم را بالا بردم. در واقع، من نشت عملکرد واقعی را اندازه‌گیری نکردم، و در صورت امکان، اگر بتوانم آن را به دست بیاورم، اجرای آن را به یک "بهینه‌تر" تغییر دادم. بنابراین، اولین اندازه‌گیری‌هایی که در زیر مورد بحث قرار گرفت، من را به‌طور جدی گیج کرد.

فکر می‌کنم بسیاری از شما، با خواندن ریشتر یا سایر ایدئولوژیست‌ها، با یک بیانیه کاملاً منصفانه برخورد کرده‌اید که بازتاب در کد پدیده‌ای است که تأثیر بسیار منفی بر عملکرد برنامه دارد.

انعکاس فراخوانی CLR را مجبور می‌کند تا از میان مجموعه‌ها عبور کند تا مجموعه مورد نیاز خود را پیدا کند، ابرداده خود را جمع‌آوری کند، آنها را تجزیه کند و غیره. علاوه بر این، انعکاس در هنگام پیمایش دنباله ها منجر به تخصیص مقدار زیادی از حافظه می شود. ما در حال استفاده از حافظه هستیم، CLR GC را آشکار می کند و فریزها شروع می شوند. باید به طور قابل توجهی کند باشد، باور کنید. حجم عظیم حافظه در سرورهای تولید مدرن یا ماشین های ابری مانع از تأخیر بالای پردازش نمی شود. در واقع، هرچه حافظه بیشتر باشد، احتمال بیشتری وجود دارد که به نحوه عملکرد GC توجه کنید. انعکاس، در تئوری، یک پارچه قرمز اضافی برای او است.

با این حال، همه ما از کانتینرهای IoC و نقشه‌نگارهای تاریخ استفاده می‌کنیم که اصل عملکرد آن‌ها نیز بر اساس بازتاب است، اما معمولاً هیچ سؤالی در مورد عملکرد آنها وجود ندارد. نه، نه به این دلیل که معرفی وابستگی ها و انتزاع از مدل های بافت محدود خارجی آنقدر ضروری است که در هر صورت باید عملکرد را قربانی کنیم. همه چیز ساده تر است - واقعاً بر عملکرد تأثیر چندانی نمی گذارد.

واقعیت این است که رایج ترین چارچوب هایی که مبتنی بر فناوری بازتاب هستند، از انواع ترفندها برای کار بهینه تر با آن استفاده می کنند. معمولاً این یک کش است. معمولاً این عبارت‌ها عبارت‌اند از عبارات و نمایندگانی که از درخت عبارت کامپایل شده‌اند. همان نقشه‌بردار خودکار یک فرهنگ لغت رقابتی دارد که انواع را با توابعی مطابقت می‌دهد که می‌توانند بدون فراخوانی بازتاب یکی را به دیگری تبدیل کنند.

چگونه این امر محقق می شود؟ اساسا، این با منطقی که خود پلتفرم برای تولید کد JIT از آن استفاده می کند، تفاوتی ندارد. هنگامی که یک متد برای اولین بار فراخوانی می شود، کامپایل می شود (و بله، این فرآیند سریع نیست)؛ در فراخوانی های بعدی، کنترل به روش قبلاً کامپایل شده منتقل می شود و هیچ کاهش عملکرد قابل توجهی وجود نخواهد داشت.

در مورد ما، شما همچنین می توانید از کامپایل JIT استفاده کنید و سپس از رفتار کامپایل شده با عملکردی مشابه با همتایان AOT آن استفاده کنید. عبارات در این مورد به کمک ما خواهند آمد.

اصل مورد بحث را می توان به اختصار به صورت زیر بیان کرد:
شما باید نتیجه نهایی بازتاب را به عنوان یک نماینده حاوی تابع کامپایل شده در حافظه پنهان ذخیره کنید. همچنین منطقی است که تمام اشیاء ضروری با اطلاعات نوع را در فیلدهای نوع خود، worker که خارج از اشیاء ذخیره می‌شوند، ذخیره کنید.

منطق در این وجود دارد. عقل سلیم به ما می گوید که اگر چیزی را بتوان کامپایل و کش کرد، باید انجام شود.

با نگاهی به آینده، باید گفت که حافظه پنهان در کار با بازتاب مزایای خود را دارد، حتی اگر از روش پیشنهادی برای کامپایل عبارات استفاده نکنید. در واقع، در اینجا من صرفاً تزهای نویسنده مقاله را که در بالا به آن اشاره کردم، تکرار می کنم.

حالا در مورد کد. بیایید به یک مثال نگاه کنیم که بر اساس درد اخیر من است که در تولید جدی یک موسسه اعتباری جدی با آن روبرو شدم. همه موجودیت ها ساختگی هستند تا کسی حدس بزند.

یک جوهر وجود دارد. اجازه دهید تماس وجود داشته باشد. حروفی با بدنه استانداردی وجود دارد که تجزیه کننده و هیدراتور همین تماس ها را از آنها ایجاد می کنند. نامه ای رسید، ما آن را خواندیم، آن را به جفت های کلید-مقدار تجزیه کردیم، یک مخاطب ایجاد کردیم و آن را در پایگاه داده ذخیره کردیم.

ابتدایی است. فرض کنید یک مخاطب دارای ویژگی های نام کامل، سن و تلفن تماس است. این داده ها در نامه منتقل می شود. این کسب‌وکار همچنین به پشتیبانی می‌خواهد تا بتواند به سرعت کلیدهای جدیدی را برای نگاشت ویژگی‌های موجودیت به جفت در متن نامه اضافه کند. در صورتی که شخصی در الگو اشتباه تایپی داشته باشد یا اگر قبل از انتشار لازم است فوراً نقشه برداری را از یک شریک جدید راه اندازی کنید و با قالب جدید تطبیق دهید. سپس می‌توانیم یک همبستگی نقشه‌برداری جدید را به عنوان یک دیتافیکس ارزان اضافه کنیم. یعنی نمونه زندگی.

ما اجرا می کنیم، تست ها را ایجاد می کنیم. آثار.

من کد را ارائه نمی دهم: منابع زیادی وجود دارد و آنها از طریق پیوند انتهای مقاله در 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();
        }

در کل مشخص است. ما خصوصیات را پیمایش می کنیم، نمایندگانی برای آنها ایجاد می کنیم که تنظیم کننده ها را فراخوانی می کنند و آنها را ذخیره می کنیم. سپس در صورت لزوم تماس می گیریم.

"آهسته" (پیشوند Slow در معیارها):

        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 را دارند تقریباً در همه پاس‌ها نسبت به روش‌هایی با پیشوند Slow کندتر هستند. این هم برای تخصیص و هم برای سرعت کار صادق است. از سوی دیگر، اجرای زیبا و ظریف نقشه برداری با استفاده از روش های LINQ که برای این کار در نظر گرفته شده است، در هر کجا که ممکن است، برعکس، بهره وری را تا حد زیادی کاهش می دهد. تفاوت در نظم است. روند با تعداد پاس های مختلف تغییر نمی کند. تنها تفاوت در مقیاس است. با LINQ 4 تا 200 برابر کندتر است، زباله های بیشتری در همان مقیاس وجود دارد.

آخرین به روز رسانی

من به چشمانم باور نداشتم، اما مهمتر از آن، همکارمان نه چشمانم را باور کرد و نه رمزم را - دیمیتری تیخونوف 0x1000000. پس از بررسی مجدد راه حل من، او به طرز درخشانی خطایی را کشف کرد و به آن اشاره کرد که به دلیل تعدادی از تغییرات در پیاده سازی از ابتدایی تا نهایی آن را از دست دادم. پس از رفع اشکال یافت شده در تنظیمات Moq، همه نتایج در جای خود قرار گرفتند. با توجه به نتایج آزمایش مجدد، روند اصلی تغییر نمی کند - LINQ همچنان بر عملکرد بیشتر از بازتاب تأثیر می گذارد. با این حال، خوب است که کار با کامپایل Expression بیهوده انجام نمی شود و نتیجه هم در زمان تخصیص و هم در زمان اجرا قابل مشاهده است. اولین راه‌اندازی، زمانی که فیلدهای استاتیک مقدار دهی اولیه می‌شوند، طبیعتاً برای روش «سریع» کندتر است، اما پس از آن وضعیت تغییر می‌کند.

در اینجا نتیجه آزمایش مجدد است:

مقاله ناموفق در مورد تسریع بازتاب

نتیجه گیری: هنگام استفاده از بازتاب در یک شرکت، نیازی به توسل به ترفندها نیست - LINQ بهره وری را بیشتر خواهد خورد. با این حال، در روش‌های با بار بالا که نیاز به بهینه‌سازی دارند، می‌توانید انعکاس را به شکل اولیه‌سازها ذخیره کنید و کامپایلرها را واگذار کنید، که سپس منطق «سریع» را ارائه می‌کند. به این ترتیب می توانید انعطاف پذیری بازتاب و سرعت برنامه را حفظ کنید.

کد بنچمارک در اینجا موجود است. هر کسی می تواند کلمات من را دوباره بررسی کند:
تست های بازتاب Habra

PS: کد در تست ها از IoC استفاده می کند و در بنچمارک ها از یک ساختار صریح استفاده می کند. واقعیت این است که در اجرای نهایی تمام عواملی را که می‌توانند بر عملکرد تأثیر بگذارند و نتیجه را پر سر و صدا کنند، قطع کردم.

PPS: با تشکر از کاربر دیمیتری تیخونوف @0x1000000 برای کشف خطای من در راه اندازی Moq، که بر اولین اندازه گیری ها تأثیر گذاشت. اگر هر یک از خوانندگان کارما کافی دارد، لطفاً آن را لایک کنید. مرد ایستاد، مرد خواند، مرد دوباره چک کرد و به اشتباه اشاره کرد. به نظر من این قابل احترام و همدردی است.

PPPS: با تشکر از خواننده دقیقی که به انتهای سبک و طراحی رسید. من برای یکنواختی و راحتی هستم. دیپلماسی ارائه چیزهای زیادی باقی می گذارد، اما من انتقاد را در نظر گرفتم. من پرتابه را می خواهم.

منبع: www.habr.com

اضافه کردن نظر