بهبود عملکرد در دات نت 11
آدرس مقاله: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/ آدرس نظرات: https://news.ycombinator.com/item?id=49711424 امتیاز: 276 # نظرات: 56
قبل از اینکه برنامههای تلویزیونی مانند The Office و Parks and Recreation این فیلم را در ذهن میلیونها نفر تثبیت کنند، کریستوفر مهمان بود. او این ژانر را اختراع نکرد، اما به طور گسترده به عنوان یکی از تأثیرگذارترین تمرینکنندگان آن شناخته میشود، و برای پول من، هیچ چیز بهتری وجود ندارد. من بیش از حد توانم تعداد Waiting for Guffman و Best in Show را تماشا کرده ام. اما چیزی که بیش از همه به من چسبیده است، چیزی که با کوچکترین تحریکی نقل می کنم، This Is Spinal Tap است.
اگر آن را دیده اید، از قبل می دانید که این به کجا می رود (و اگر ندیده اید، اکنون برنامه های آخر هفته دارید). این فیلم یک مستند تخیلی درباره یک گروه راک انگلیسی قدیمی به نام اسپینال تپ است که اعضای آن هر چیزی هستند که وقتی از ستارههای راک فوقالعاده تصویر میکنیم، تصور میکنیم. در یکی از صحنههای به یاد ماندنیتر، گیتاریست (نایجل) به فیلمساز (مارتی) توری از باارزشترین تجهیزاتش میدهد، بهویژه تقویتکنندهای را نشان میدهد که شبیه هیچکدام از موارد دیگر نیست: صفحههای آن روی ده متوقف نمیشوند. این منجر به چیزی می شود که ممکن است بیشترین نقل قول در کل فیلم باشد:
نایجل: "میدانی، میدانی، بیشتر جوانها، میدانی، در ساعت ده خواهند نواخت. تو روی ده اینجا، تمام راه بالا، تمام راه بالا، تمام راه بالا، تو ده روی گیتار خودت هستی. از آنجا کجا میتوانی بروی؟ کجا؟"
نایجل: "هیچ جا. دقیقا. کاری که ما انجام می دهیم این است که اگر به آن فشار اضافی روی صخره نیاز داشته باشیم، می دانید چه کار می کنیم؟"
این .NET 11 است. صدایی بلندتر از یک سال دیگر انجام شده است که زمان اجرا و کتابخانه ها را بسیار سریعتر کرده است. البته، فرض تقویت کننده ویژه نایجل مسخره است، همانطور که در چند خط گفتگوی بعدی نشان داده شده است:
مارتی: "چرا فقط ده را بلندتر نمی کنی و ده را بالاترین عدد نمی کنی و آن را کمی بلندتر نمی کنی؟"
در مقابل، دات نت 11 در واقع یک بالاتر، یک بلندتر است. بخش های بعدی پر از پیشرفت های واقعی است. یک بررسی کرانهها حذف شد، یک تخصیص که دیگر اتفاق نمیافتد، یک قفل که گرفته نمیشود، یک حلقه که در چرخههای کمتری نسبت به یک سال پیش اجرا میشود، یک مقایسه به یک ثابت در اینجا، یک بررسی اضافی که از یک حلقه در آنجا بلند شده است، یک دو دستورالعمل در یکی، یک syscall کنار گذاشته شده، یک آرایه روی MD کپی و خاموش میشود.
کار اجرای واقعی اینگونه پیش میرود، سود پس از سود جمع میشود، هر کدام در آخرین بار ترکیب میشوند، تا زمانی که کل کار بهطور قابل اندازهگیری و بهطور قابل اثباتی بلندتر شود. و بنابراین، در این پست، همانطور که در سالهای گذشته با .NET 10، .NET 9، .NET 8، .NET 7، .NET 6، .NET 5، .NET Core 3.0، .NET Core 2.1، و .NET. از آنها
این یک طولانی است. قرار است باشد. نوشیدنی داغ دلخواه خود را بگیرید، در آن مستقر شوید و بیایید آن را افزایش دهیم.
مانند سالهای گذشته، پست مملو از معیارهای خرد است که پیشرفتهای فردی را نشان میدهد. تقریبا همه آنها از BenchmarkDotNet استفاده می کنند و هر کدام به صورت مستقل نوشته شده اند تا بتوانید خودتان آن را امتحان کنید.
با اطمینان از نصب هر دو .NET 10 و .NET 11 شروع کنید (بیشتر معیارها کدهای مشابهی را که در هر دو نسخه اجرا می شود مقایسه می کنند) و یک پروژه کنسول جدید در فهرست بنچمارک های تازه ایجاد کنید:
محتویات benchmarks.csproj تولید شده را با موارد زیر جایگزین کنید، که هر دو نسخه را چند هدف قرار می دهد تا BenchmarkDotNet بتواند برای هر کدام بسازد:
برای آزمایش یک معیار معین، محتویات کامل آن را روی همه چیز در Program.cs کپی کنید و سپس آن را اجرا کنید. هر بنچمارک به عنوان یک نظر در بالا دستور دقیقی را که باید استفاده شود را شامل می شود. در بیشتر موارد، این است:
که در Release ساخته می شود و معیار را در برابر هر دو دات نت 10 و نت 11 اجرا می کند و مقایسه ای کنار هم ارائه می کند. شکل متداول دیگری که برای مقایسه دو رویکرد کدنویسی در یک زمان اجرا (به جای یک کد در دو زمان اجرا) از یک بنچمارک استفاده می شود:
سلب مسئولیت معمول اعمال می شود: اینها معیارهای خرد هستند، بسیاری از عملیات اندازه گیری آنقدر کوتاه هستند که یک پلک زدن آنها را از دست می دهد. نتایج شما با سخت افزار، سیستم عامل، پیکربندی زمان اجرا، کارهای دیگری که دستگاه شما دقیقا در آن لحظه انجام می دهد و اینکه آیا عطارد در حالت رتروگراد قرار دارد، متفاوت خواهد بود.
هر خط کد مدیریت شده در نهایت به کامپایلر به موقع ختم می شود، بنابراین بیایید از آنجا شروع کنیم.
از میان همه مکانهایی که برای بهبود عملکرد .NET وجود دارد، تعداد کمی از آنها به اندازه کامپایلر JIT (JIT) تأثیر گستردهای دارند. C#، F# و Visual Basic معمولا ابتدا به زبان میانی (IL) کامپایل میشوند و JIT در نهایت آن IL را به دستورالعملهای بومی که CPU اجرا میکند تبدیل میکند. بنابراین، بهبود JIT میتواند در هر کجا که الگوی بهینهسازی شده اتفاق میافتد، برای کد برنامه و کتابخانه مفید باشد، اغلب بدون تغییر منبع یا کامپایل مجدد خود برنامه. حتی حذف یک دستورالعمل یا اثبات غیرضروری یک چک می تواند زمانی که کد در مسیر بسیار داغ قرار دارد، جمع شود.
ما به عنوان توسعه دهندگان انتزاعات خود را دوست داریم. آنها به ما اجازه میدهند کد شی گرا و تمیز، قابل استفاده مجدد بنویسیم، اما ما نمیخواهیم برای هر انتزاعی در زمان اجرا هزینه بپردازیم. زمان اجرا اغلب زمانی می تواند یک انتزاع را خنثی کند که ثابت کند اثرات قابل مشاهده نیستند. میتواند به یک فراخوانی مجازی نگاه کند و تعیین کند که کدام روش بتن را فراخوانی میکند، به تخصیص پشته نگاه میکند و تشخیص میدهد که شی هرگز از قاب پشته فعلی خارج نمیشود، یا به یک رابط ریختهشده نگاه کند و از یک واقعیت نوع که قبلا در روش ایجاد شده است، دوباره استفاده کند.
به این فرآیند "تخریب" می گویند. دات نت سال ها در این زمینه به طور پیوسته بهبود یافته است و این در .NET 11 نیز ادامه دارد.
هر بار که رابطی را در سی شارپ می نویسید، یک قرارداد ایجاد می کنید، این قول را می دهید که هر نوع رابطی که آن رابط را اجرا می کند می تواند جایگزین هر نوع دیگری شود. این انعطافپذیری بسیار ارزشمند است زیرا، برای مثال، چیزی است که به ما امکان میدهد IEnumerable<T> را بنویسیم و روی آرایهها، لیستها، مجموعههای دیگر، LINQ، تکرارکنندههای سفارشی و غیره به خوبی کار کند. اما CPU چیزی در مورد این قراردادها نمی داند. فقط می داند که چگونه دستورالعمل ها را اجرا کند. تبدیل «هر روشی که این رابط مرجع به آن اشاره میکند فراخوانی کنید» به دستورالعملهای واقعی ماشین به ماشینآلات خاصی نیاز دارد.
این مثال را در نظر بگیرید:
در زمان کامپایل، همه چیز برابر است، JIT نمی داند که _animal سگ است یا گربه. کدی تولید میکند که «نشانگر جدول متد» نمونه (دسته نوع شی آن)، که گاهی اوقات «اشارهگر vtable» نامیده میشود، در ابتدای هر شی داتنت ذخیره میشود، در جدول متد در اسلات شناختهشده برای Speak ایندکس میشود و نشانگر تابع موجود در آنجا را فراخوانی میکند:
برای این یک تماس با Speak، ما سه عدم ارجاع حافظه وابسته و یک تماس غیرمستقیم را پرداخت میکنیم، زیرا پردازنده از قبل به طور قطعی نمیداند که تماس به کجا میرود (ممکن است حدس بزند، یا «به طور فرضی اجرا شود»، اما باید برای احتمال اشتباه بودن آن آماده شود)، و چون هدف تماس غیرمستقیم است، JIT نمیتواند تماس گیرنده را درون خطی قرار دهد. هر کاری که Speak انجام دهد، کد آن را نمی توان در روش فراخوانی تا کرد.
این یک مشکل عملکردی است. این مسیرهای غیرمستقیم سربار دارند، اما هزینه بیشتر، فرصت از دست رفته برای درون خطی است. Inlining نه تنها سربار فراخوانی تابع را ذخیره می کند، بلکه مهمتر از آن، کد تماس گیرنده را تا همان بهینه سازی هایی که روی تماس گیرنده عمل می کنند، مانند انتشار ثابت، حذف کد مرده، حذف بررسی کرانه ها، غیر مجازی سازی بیشتر و غیره باز می کند.
این بدان معناست که یک سری تماسهای مجازی کوچک که هرکدام بیگناه به نظر میرسند، وقتی مجازیسازی میشوند و خطبندی میشوند، میتوانند به تعداد انگشت شماری از دستورالعملها تبدیل شوند که در مقایسه با کد منبع اصلی غیرقابل تشخیص و ارزانتر هستند. بدون خط کشی، هر گیره یک جعبه مات است. با آن، JIT می تواند از طریق لایه ها را ببیند.
ما بهعنوان توسعهدهندگان داتنت دائما به اکتشافیهای پیچیده JIT برای خطبندی که اندازه IL تماس گیرنده، کار دقیقی که تماس گیرنده انجام میدهد، فرکانس فراخوانی روش، سود مورد انتظار از آرگومانهای ثابت و دهها عامل دیگر، متکی هستیم. برای تماس های مجازی، JIT باید بداند که هدف واقعی تماس چه خواهد بود. نیاز به «مجازی زدایی» دارد. در برخی موارد، می تواند آن را به صورت ایستا تعیین کند، جایی که دانش دقیق نوع دارد. به عنوان مثال، اگر JIT بتواند ثابت کند که حیوان همیشه یک سگ است، چه به این دلیل که به تازگی با Dog() جدید تخصیص داده شده است:
یا چون نوع متغیر یک کلاس مهر و موم شده است:
یا با NativeAOT و کامپایل کل برنامه، اگر ببیند که Animal انتزاعی است و تنها نوع در کل برنامه که از Animal مشتق شده است Dog است:
یا اعتبار سنجی دیگری از این دست، می تواند مستقیما به Dog.Speak() فراخوانی ارسال کند و inliner می تواند عکس آن را بگیرد.
اما برای موارد دیگری که نمی تواند این را با تجزیه و تحلیل استاتیک ثابت کند، JIT به بهینه سازی هدایت شده پروفایل (PGO) روی می آورد. PGO فانتزی به نظر می رسد، اما از نظر مفهومی ساده است. با "کامپایل لایهای"، زمانی که یک روش برای اولین بار فراخوانی میشود، میتوان آن را "در زمان مقرر" با بهینهسازیهای کمبههنگام کامپایل کرد (این به عنوان ردیف 0 نامیده میشود).
JIT میتواند در این مجموعه کاوشگرهای اضافی را شامل شود (به «اشکالزدایی printf» فکر کنید) که به آن اجازه میدهد دستهای از اطلاعات جالب در مورد ماهیت کد را ردیابی کند، آنچه را که واقعا هنگام اجرای آن اتفاق میافتد را ثبت کند: کدام شاخهها گرفته میشوند، چه نوع بتنهایی در سایتهای تماس مجازی یا تلاشهای ارسال محتوا نشان داده میشوند، و غیره. اگر روش به اندازه کافی فراخوانی شود یا بارها به اندازه کافی حلقه شود، زمان اجرا می تواند از JIT بخواهد یک نسخه بهینه شده جدید (به عنوان Tier 1) تولید کند. سپس آن مجموعه میتواند در تمام آموختههای جمعآوریشده به عنوان بخشی از آن نمایهسازی نقش داشته باشد.
البته JIT همچنان نیاز به تولید کدی دارد که همیشه صحیح باشد. حتی اگر یک نمایه پویا بگوید که حیوان 100٪ مواقع سگ بوده است، این تضمین نمی کند که در آینده همیشه سگ باشد. ممکن است 1000 تماس اول در یک سگ انجام شود اما 1001 تماس در دلفین انجام شود. چگونه JIT می تواند این یادگیری را در آن گنجانده باشد؟ با ارسال چک زمان اجرا. مسیر Dog میتواند یک تماس مستقیم دریافت کند، که ممکن است غیرقابل اتصال باشد، و مسیر دیگر تماس مجازی اصلی را بهعنوان بازگشتی نگه میدارد.
سرعت از کوچک کردن کیس معمولی ناشی میشود، در حالی که صحت از دست نخورده ماندن مورد غیر معمول ناشی میشود.
این الگوی «حدس بزنید و تأیید کنید»، که «مجازیزدایی محافظتشده» (GDV) نامیده میشود، بسیاری از بزرگترین بردهای توان عملیاتی را در حجمهای کاری واقعی به حساب میآورد. این نه تنها برای ارسال مجازی بلکه برای ارسال رابط نیز قابل استفاده است، که اتفاقا کمی گرانتر از ارسال مجازی است زیرا یک نوع میتواند هر تعداد اینترفیس را پیادهسازی کند و این بدان معناست که شکافهای رابط به سادگی به موقعیتهای ثابت vtable نگاشت نمیشوند.
هنگامی که آشکار میکند که چه نوع شیئی درگیر است، انتزاعزدایی میتواند ایجاد شی را کارآمدتر کند. به طور کلی، اشیاء در دات نت بر روی زباله های جمع آوری شده تخصیص داده می شوند، توسط جمع کننده زباله (GC) ردیابی می شوند و زمانی که دیگر در دسترس نیستند جمع آوری می شوند. تخصیص هیپ معمولا سریع است، اغلب به طور مؤثر فقط با یک اشاره گر برخورد می کند. با این حال، زمانی که فضای کافی برای ضربه زدن به نشانگر وجود نداشته باشد، می تواند بسیار گران تر شود، از جمله نیاز به جمع آوری زباله.
هر شی تخصیص یافته همچنین هزینه مستهلک شده همه مجموعه ها را متحمل می شود، زیرا هر شی تخصیص یافته در نهایت باید تمیز شود.
"Escape Analysis" تکنیک کامپایلری است که به ما امکان میدهد بپرسیم آیا این شی هرگز از روش فعلی "گریز" میکند یا خیر. اگر یک ارجاع شی به یک شی جدید تخصیص یافته به طور قابل اثباتی فرار نکند، آنگاه JIT می تواند آن را با کارایی بیشتری تخصیص دهد. نیازی نیست آن را در پشته GC ذخیره کند، زیرا هیچ چیز احتمالا نیازی به ارجاع مجدد به آن شیء ندارد، بنابراین می تواند در عوض شیء را در پشته اختصاص دهد، و هم تخصیص و هم پاکسازی را اساسا رایگان می کند. تخصیص پشته حتی سریعتر از تخصیص نشانگر ضربهای پشته است. این فقط نشانگر پشته را کاهش می دهد، که معمولا قبلا در یک ثبات است.
و مهمتر از آن به معنی صفر بودن ضربه GC است، زیرا قاب پشته در بازگشت عملکرد به صورت اتمی آزاد می شود.
JIT به تدریج در حال گسترش تجزیه و تحلیل فرار در چند نسخه اخیر دات نت بوده است. موضوع کلیدی این است که هر فرار مثبت کاذب، هر بار که JIT به اشتباه نتیجه میگیرد که ممکن است یک شی فرار کند در حالی که واقعا چنین نیست، نشان دهنده تخصیص پشتهای است که میتوانست از آن جلوگیری شود، و ما میخواهیم آن لیست مثبت کاذب را کم کنیم. در دات نت 11، JIT این لیست را به روش های مختلفی کاهش می دهد.
ما با بوکس پوچ شروع می کنیم. این معیار را در نظر بگیرید:
dotnet/runtime#122167 جعبههای پوچپذیر را در داخل JIT گسترش میدهد و کادر موقت را برای فرار از تجزیه و تحلیل قرار میدهد. قبلا یک راهنما زمان اجرا آن را مخفی می کرد. برای ورودی تهی، هیچ تخصیصی در هر دو نسخه وجود ندارد، زیرا هیچ چیز جعبه نمی شود. و در هر دو نسخه، BoxNullableValue شی جعبه شده را برمی گرداند، به این معنی که شی فرار می کند، بنابراین تخصیص 24 بایت باقی می ماند. با این حال، برای FormatNullableInt، JIT در NET 11 اکنون میتواند ببیند که کادر موقت 24 بایتی فرار نمیکند و آن تخصیص پشته را به طور کامل حذف میکند.
تجزیه و تحلیل فرار از طریق مکانیزمی به نام تجزیه و تحلیل فرار مشروط (CEA) برای شمارش کنندگان بیشتر بهبود یافت. پشتیبانی از CEA در دات نت 10 معرفی شد، اما دات نت 11 مجموعه الگوهایی را گسترش می دهد که این تحلیل می تواند با خیال راحت تشخیص دهد. تجزیه و تحلیل فرار موجود می پرسد که آیا یک مرجع ایجاد شده توسط یک تخصیص می تواند در جایی جریان یابد که JIT دیگر نتواند آن را ردیابی کند، مانند یک تماس ناشناخته. اگر بتواند، جسم باید روی پشته باقی بماند.
آن تحلیل لزوما محافظه کارانه است و تا حد زیادی به جریان حساس نیست: اگر یک شی ممکن است به یک فراخوانی واسط در هر مسیری ارسال شود، سعی نمیکند ثابت کند که مسیر حاوی آن فراخوانی با مسیر حاوی تخصیص منحصر به فرد است.
متأسفانه، این دقیقا همان چیزی است که GDV هنگامی که یک foreach را بر روی IEnumerable<T> بهینه میکند، تولید میکند. همانطور که قبلا ذکر شد، GDV یک فراخوانی رابط را به یک چک نوع با دو شاخه تبدیل میکند: یک شاخه سریع برای نوع مجموعه احتمالی و یک شاخه بازگشتی حاوی فراخوانی رابط اصلی. مجازیسازی و درونسازی در امتداد شاخه سریع اغلب تخصیص شمارشگر را برای نوع مجموعه نشان میدهد، در حالی که گاردهای شمارشگر بعدی فراخوانهای برگشتی مانند IEnumerator<T>.MoveNext را حفظ میکنند.
تجزیه و تحلیل موجود آن تماس ها را می بیند و نتیجه می گیرد که شمارشگر محلی تخصیص داده شده ممکن است فرار کند. در عوض CEA رابطه بین تخصیص مسیر سریع و شمارشگر محلی که توسط محافظان بعدی آزمایش شده است را ثبت می کند. اگر هر فرار ظاهری فقط پشت یک بررسی نوع ناموفق اتفاق بیفتد، JIT میتواند منطقه را به نسخه داغی شبیهسازی کند که در آن بررسیها موفق هستند. در آن کلون، شیء نمی تواند به فراخوانی های بازگشتی برسد، بنابراین می توان آن را به صورت پشته ای تخصیص داد و اغلب به محلی های اسکالر جداگانه ارتقا داد. منطقه اصلی به عنوان مسیر آهسته عمومی باقی می ماند.
با این حال، یکی از مواردی که .NET 10 از پس آن برنمیآید، پیادهسازی GetEnumerator() بود که نتیجه فراخوانی GetEnumerator() دیگر را برمیگرداند. برای مثال، عبارت مجموعهای که به IEnumerable<int> تبدیل میشود، از یک بستهبندی آرایه فقط خواندنی تولید شده توسط کامپایلر با دقیقا این ساختار استفاده میکند: GetEnumerator() wrapper به GetEnumerator آرایه زیربنایی تفویض میکند. با dotnet/runtime#122946، JIT در NET 11 این «زنجیرهسازی» را مدیریت میکند:
ReadOnlyStatic، که JIT میتواند بهطور مؤثر فیلد فقط خواندنی آن را بهعنوان یک ثابت در نظر بگیرد، قبلا در NET 10 بهینهسازی شده بود. در NET 11، مورد فیلد نمونه نیز تخصیص شمارشگر 32 بایتی خود را از دست میدهد و در همان توان عملیاتی همگرا میشود.
dotnet/runtime#121918 از @MichalPetryka راه دیگری را تصحیح میکند که آدرس میتواند بیضروره یک شی را فراری نشان دهد. IL محدود کرد. پیشوند اجازه می دهد تا یک توالی callvirt عمومی برای هر دو نوع مقدار و انواع مرجع کار کند: می تواند از جعبه کردن یک نوع مقدار جلوگیری کند، در حالی که برای یک نوع مرجع، گیرنده را ارجاع داده و ارسال مجازی معمولی را انجام می دهد. ObjectEqualityComparer<T>.Equals، که در معیار زیر توسط EqualityComparer<T> استفاده شده است. پیش فرض، حاوی چنین فراخوانی به value.Equals(other) است. گیرنده به عنوان خوانده شده غیر مستقیم از طریق آدرس یک محلی نشان داده شد.
صرفا گرفتن آن آدرس، محلی را به عنوان در معرض علامت گذاری می کند، و از در نظر گرفتن مقدار جدید تخصیص یافته برای تخصیص پشته جلوگیری می کند. گیرنده اکنون به عنوان یک بار مقدار مستقیم نشان داده می شود و تخصیص پشته 24 بایتی ناپدید می شود.
در حالی که CEA می تواند یک شی غیر فرار را از پشته GC خارج کند، گاهی اوقات JIT می تواند جلوتر رفته و ثابت کند که اصلا نیازی به تخصیص نیست. کد عمومی منبع مشترکی از چنین فرصت هایی را از طریق بوکس فراهم می کند. به عنوان مثال، متد ArgumentNullException.ThrowIfNull یک مقدار شی را می پذیرد. یعنی وقتی روشی مثل این دارید:
هنگامی که T به یک ساختار غیر قابل تهی محدود می شود، برای ارسال مقدار به عنوان شیء، boxing ایجاد می شود. ThrowIfNull در اینجا یک nop است اگر مقدار غیر تهی باشد (زیرا روش به سادگی if است (مقدار null است) Throw(); ) و نسخه های قبلی با موفقیت آن جعبه را در کد بهینه شده بهینه کردند. با این حال، در Tier 0، این بهینهسازی اعمال نشد و ThrowIfNull در نهایت تخصیص مییابد. در حالی که این امر بر توان عملیاتی حالت پایدار تأثیر منفی نمیگذارد، اما منجر به ایجاد نویز آزاردهنده در نمایهسازی و همچنین سربار اضافی در طول راهاندازی میشود، جایی که چنین استفادهای هنوز از سطح 0 ارتقاء نیافته است.
در NET 11، dotnet/runtime#129392 پشتیبانی از این را در Tier 0 نیز اضافه می کند.
در سمت اعزام مجازی، چندین PR به بهبود روشهای مجازی عمومی (GVM) کمک میکنند. dotnet/runtime#120866 از @hez2010 از ریختن مشتاقانه اهداف تماس ldvirtftn به یک تماس موقت جلوگیری میکند و به وضوح هدف مجازی عمومی اجازه میدهد تا در صورت قانونی، جلوتر از تنظیم آرگومان حرکت کند. dotnet/runtime#122023 از @hez2010 سپس JIT را قادر میسازد تا GVMهای غیر اشتراکگذاری شده را مجازیسازی کند و زمینه عمومی مورد نیاز برای تبدیل ارسال غیرمستقیم به تماس مستقیم و بالقوه غیرقابلخط را داشته باشد.
و dotnet/runtime#128702 از @hez2010 این پشتیبانی را به GVM های مشترک و پیاده سازی های رابط پیش فرض که نیاز به یک خرده نمونه دارند، گسترش می دهد. این بهینهسازیها میتوانند اندازه کل کد را زمانی که تماسهای مستقیم جدید به صورت خطی انجام میشوند، افزایش دهند، اما این به طور کلی معامله مورد نظر است: بیشتر کار واقعی برای بهینهساز قابل مشاهده است. معیار زیر را در نظر بگیرید:
فرستادن یک پردازشگر تازه تخصیص داده شده به IProcessor یک تماس مجازی عمومی رابط را در IL ایجاد می کند، اما JIT اکنون قادر است نوع دقیق گیرنده را، حتی در مورد رشته مشترک، مشاهده کند، به طوری که .NET 11 هر دو تماس را غیر مجازی و درون خطی می کند. این به نوبه خود Unsafe.SizeOf<T>() را به عنوان یک ثابت نشان می دهد و ثابت می کند که پردازنده کوتاه مدت اصلا نیازی به تخصیص ندارد.
با تکیه بر آن، dotnet/runtime#123183 از @hez2010 کامپایل ReadyToRun را قادر میسازد تا تماسهای مجازی عمومی غیر مشترک بیشتری را حل و فصل کرده و مجازیسازی کند که در غیر این صورت غیرمستقیم باقی میماندند، و dotnet/runtime#130202 از @hez2010 این پشتیبانی را به NativeAOT گسترش میدهد. NativeAOT برخی از اهداف مجازی عمومی را بهعنوان «نشانگرهای چربی» نشان میدهد (اشارهگرهایی که بیش از یک آدرس هستند، معمولا یک آدرس و ابردادههای مرتبط هستند، و در این مورد هم آدرس کد و هم زمینه عمومی را دارند). با به تعویق انداختن آن تبدیل تا زمانی که غیر مجازی سازی از نوع دقیق فرصتی برای اجرا داشته باشد، JIT
می تواند یک سایت تماس واسط با یک هدف شناخته شده واحد را به یک GVM غیر اشتراکی به یک تماس مستقیم تبدیل کند که ممکن است سپس به صورت خطی انجام شود.
اطلاعات نوع نیز برای زنده ماندن از تغییراتی که JIT در داخل انجام می دهد، نیاز دارد. اگر JIT در حین بازسازی یک درخت، یک عبارت مرجع را به یک عبارت موقت بریزد، از دست دادن اطلاعات کلاس دقیق عبارت میتواند تماسی را که غیر مجازیسازی شده بود به یک تماس مجازی غیرشفاف تبدیل کند. این چیزی است که در اینجا در NET 10 اتفاق میافتد: مقدار جعبه میشود و SetValue از طریق IValue فراخوانی میشود. dotnet/runtime#128485 از @hez2010 دسته کلاس و دقت را در حالت موقت حفظ می کند.
با این اطلاعات که هنوز در دسترس است، دات نت 11 تماس را غیر مجازی و خطی می کند و جعبه و تخصیص 24 بایت آن را حذف می کند.
متن اصلی (انگلیسی)
Performance Improvements in .NET 11
Article URL: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/ Comments URL: https://news.ycombinator.com/item?id=49711424 Points: 276 # Comments: 56