امیر محمد مشایخی
امیر محمد مشایخی
خواندن ۸ دقیقه·۳ سال پیش

دارت و مزایای عملکردی Sound types

استفاده از soundness و null safety برای ایجاد کد کوچک تر و سریعتر

کد (اسمبلی) تولید شده از از توابع یکسان دارت، در ورژن های 1.24، 2.0، و 2.12 (از سمت چپ به راست) که کوچکتر شده. برای دیدن چرایی ماجرا (و مشاهده کد تولید شده واقعی)، به خواندن ادامه دهید.
کد (اسمبلی) تولید شده از از توابع یکسان دارت، در ورژن های 1.24، 2.0، و 2.12 (از سمت چپ به راست) که کوچکتر شده. برای دیدن چرایی ماجرا (و مشاهده کد تولید شده واقعی)، به خواندن ادامه دهید.


ما سیستم نوع داده ای (type system) دارت را در طول چند سال گذشته قدرتمند کرده ایم. زبان دارت اصیل (دارت نسخه 1) یک سیستم نوع داده ای unsound اختیاری داشت. (مشابه لهجه های typed Javascript مانند TypeScript متعلق به مایکروسافت یا Flow متعلق به فیسبوک ). دارت 2 یک سیستم sound type سختگیرانه تر را معرفی کرد. در طول دو سال گذشته، ما داشتیم روی توسعه دادن بیشتر تایپ سیستم ، به وسیله sound null safety کار می‌کردیم.

در حالی که یک sound type system اعتماد به نفس بیشتری به توسعه دهندگان می‌دهد، compiler ما را نیز قادر می‌سازد که به شکل بی خطری انواع داده را برای ایجاد کد بهینه استفاده کند. با soundness، ابزار های ما ضمانت می‌دهند که تایپ ها از طریق ترکیبی از بررسی های ایستا و زمان اجرا (runtime checking) (اگر نیاز شد) درست هستند. بدون soundness، بررسی نوع داده فقط تا اینجا می‌تواند پیش برود، و تایپ های static ممکن است در زمان اجرا اشتباه باشند.

در عمل، soundness به compiler ما اجازه می‌دهد تا کد کوچک تر و سریعتری ایجاد کند، خصوصا در یک تنظیم ahead-of time (AOT)، جایی که ما کد از پیش کامپایل شده بومی را به کاربران ارسال می‌کنیم.

یک مثال

تابع مثال زیر نشان می‌دهد که چطور sound types می‌توانند تاثیر چشمگیری را روی کد نسبتا ساده ای داشته باشند :

int getAge(Animal a) { return a.age; }

در آخرین نسخه stable دارت 1 (1.24.3)، این تابع به 26 دستور العمل x64 بومی تبدیل می‌شود - و این فقط بعد از بهینه سازی های ابزاری (instrumentation) و هدایت شده به وسیله profile بود، که سرعت بالا آمدن آغازین زمان اجرا را کند می‌کند. با sound null safety در دارت 2.12، این کد فقط به 3 دستور العمل تبدیل می‌شود. بدون هیچ نیازی به بهینه سازی هدایت شده توسط profile.

دارت به هر دوی ARM 32/64 و معماری (های) x86/x32 کامپایل می‌شود. در مثال زیر، ما x64 را استفاده می‌کنیم، اما نتایج روی اهداف دیگر مشابه هستند.

کد کامل دارت و محتوای تابع مثال در پایان این مقاله نشان داده شده اند، اما در اینجا نکاتی کلیدی وجود دارند:

کلاس Animal حاوی یک فیلد age از نوع int است.

  • کلاس Animal چندین زیر کلاس دارد (Cat, Dog, Snake, Hamster).
  • تابع بالا روی خیلی از این تایپ ها در زمان اجرا فرا خوانده می‌شود.

چیدمان object دارت

کلاس Animal دارت، زمانی که به کد بومی (x64) کامپایل می‌شود، چیدمان ساده ای دارد :

چیدمان کلاس Animal
چیدمان کلاس Animal

8 بایت اول یک هدر هستند که اطلاعات reified type را ارائه می‌دهند (که تایپ زمان اجرای object است). 8 بایت دوم حاوی فیلد age هستند. همه زیرکلاس ها (و به طور بالقوه اضافه بر) این ساختار را حفظ می‌کنند: هر یک از فیلد های اضافه بعد از آن گذاشته می‌شوند، ساختار اصلی تایپ را حفظ می‌کند.

تابع getAge، به شیء Animal داده شده (یا هر کلاس فرزندی) باید فیلد را از یک انحراف 8 بایتی لود کند و آن را بازگرداند.

دارت نسخه 1 : Unsound Types

در دارت 1، با این حال تایپ های ایستا (static) sound نبودند و به طور موثری در حین کامپایل نادیده گرفته می‌شدند. در زمان اجرا، ما نمی‌توانستیم در نظر بگیریم که تایپ ایستا (static) صحیح باشد (و از این رو، چیدمان آن همان طوری بود که انتظار میرفت). دسترسی به age ممکن است به یک فیلد در موقعیتی متفاوت باشد، به یک getter که جلوتر توسط کد قابل اجرا استفاده می‌شود، یا به یک فیلد ناموجود (که یک خطای قابل پیشگیری را در زمان اجرا رهاسازی می‌کند).

دارت 1 طراحی شده بود که به کامپایلر JIT (Just-In-Time) و ماشین مجازی بر روی دستگاه کاربر تکیه کند، که کد را با استفاده از اطلاعات تایپ زمان اجرا بهینه سازی می‌کند. در این طرح، ما در واقع هر تابعی را دو بار کامپایل کردیم: اولین، برای جمع آوری اطلاعات، و دومین (برای توابع حاد) به منظور تولید کد بهینه شده تر بر اساس رفتار مشاهده شده زمان اجرا.

دارت نسخه 1: اولین کامپایل

اولین کامپایل برای getAge، این 47 دستور العمل زیر را روی معماری x64 تولید کرد.

خروجی اسمبلی حاصل از کامپایل
خروجی اسمبلی حاصل از کامپایل

توجه داشته باشید که این کد برای تعیین اینکه چه چیزی در زمان اجرا اتفاق می‌افتد استفاده شده است. این هیچ چیزی درباره شیء داده شده در نظر نمی‌گیرد و به شکل موثری معادل جستجوی یک جدول هش را برای یافتن درست فیلد اجرا می‌کند، یک getter را اجرا کرده یا یک خطا را پرتاب می‌کند.

دارت نسخه 1: کامپایل دوم

در این مورد، کد به طور مکرر فراخوانی می‌شود و یک ثانیه را رقم می‌زند. بهینه سازی کامپایل که 26 دستور العمل زیر را تولید می‌کند :

خروجی اسمبلی حاصل از کامپایل
خروجی اسمبلی حاصل از کامپایل

کد بهینه سازی شده هنوز واقعا بزرگ است. این بر اساس اطلاعات profile است که نشان می‌دهد تابع فقط روی نمونه کلاس های Cat ،Hamster و Dog اینووک (invoke) شده است، و با فرض اینکه همین امر در آینده نیز یکسان خواهد بود بهینه شده است.

کد آبی رنگ prologue و epilogue برای تابع است (برای تنظیم و بازگردانی فریم پشته (stack frame)). کد قرمز رنگ موارد مورد انتظار را بررسی می‌کند - که شیء پوچ نباشد (non-null) و یکی از تایپ های دیده شده در گذشته باشد - و یک مسیر کند را برای موارد دیگر invoke می‌کند. کد پر رنگ کار حقیقی برای لود کردن فیلد است.

کد بهینه شده در واقع ممکن است کند تر باشد اگر رفتار آینده نسبت به گذشته متفاوت باشد: اگر getAge روی یک نمونه جدید invoke شود (مثل یک نمونه از کلاس Snake) کد بررسی های اضافه را اجرا می‌کند اما همچنان در مسیر کند قرار می‌گیرد.

مشکلات کد تولید شده در دارت نسخه 1

کد تولید شده بالا خیلی به ساختاری که امروزه توسط V8 تولید می‌شود شباهت دارد، موتور JavaScript در کروم، زمانی که یک برنامه کم و بیش معادل JavaScript/TypeScript/Flow به آن داده می‌شود. در حالی که این رویکرد (و کد متناظر تولید شده) می‌تواند کارکرد خوبی در خیلی از سناریو ها ارائه دهد، این برای جایی که ما شروع به هدف قرار دادن یک مجموعه گسترده از پلتفرم های مشتریان (به ویژه با فلاتر) کردیم مناسب نبود، شامل دستگاه ها موبایلی که نسبت به ردپای مموری (memory footprint) و حجم بیشتر حساس هستند :

  • اول، بهای کامپایل در client-side به طور کلی ردپای برنامه های دارت را افزایش داد.
  • دوم، بهای کامپایل دو فاز نظری برای اجرای برنامه زیان آور بود.
  • سوم، کامپایل Just-In-Time روی IOS مجاز نیست: ما نیاز به یک استراتژی جانشین برای حداقل بعضی از هدف ها داشته باشیم.

ما در عوض به یک رویکرد کامپایل Ahead-Of-Time روی آوردیم. اما با دارت نسخه 1 باعث نتیجه گرفتن کدی به طور قابل توجه بدتر شد. حتی با sophisticated، تحلیل و بررسی تمام برنامه، ما نتوانستیم همیشه اطلاعات مربوط به تایپ را در زمان کامپایل تعیین کنیم، به ویژه با بزرگتر شدن برنامه ها. به اضافه، بهای حدس و گمان - کد قرمز رنگ بالا - زمانی که کل برنامه از پیش کامپایل شد هزینه بر شد.

دارت نسخه 2: Sound Types

با دارت 2، ما soundness را معرفی کردیم، که به ما امکان کامپایل امن کد را بر اساس اطلاعات تایپ داد و تکیه ما به profiling برای performance را کاهش داد. با دارت 2، با یک کامپایل Ahead-Of-Time تکی، ما 10 دستور العمل روی معماری x64 تولید کردیم:

خروجی اسمبلی حاصل از کامپایل
خروجی اسمبلی حاصل از کامپایل

این کد هنوز بررسی null را انجام می‌دهد (به رنگ قرمز) و تابع helper را فراخوانی می‌کند اگر که مقدار null یافته شد.

دارت نسخه 2.12: Sound null safety

با sound null safety، تایپ سیستم غنی تر است، و کامپایلر ما می‌تواند از آن استفاده کند. کامپایلر می‌تواند به شکل امن به تایپ non-nullable تکیه کند و کد قرمز بالا (که مربوط به null-check بود) را از بین ببرد. در دارت 1.12 بتا، ما 3 دستور العمل کمتر تولید می‌کنیم :

خروجی اسمبلی حاصل از کامپایل
خروجی اسمبلی حاصل از کامپایل

در واقع، کد بالا که ساده تر می‌شود، ما همچنین توانسته ایم که prologue و epilogue را ساده و موثر کنیم. در انتشار stable آینده مان، ما فقط 3 دستورالعمل برای تابع مثال تولید خواهیم کرد :

خروجی اسمبلی حاصل از کامپایل
خروجی اسمبلی حاصل از کامپایل

با sound null safety، ما می‌توانیم کد تولید شده برای این تابع را بر اساس آن کاهش دهیم: یک لود فیلد. در عمل، یک فراخوانی به این تابع همیشه در یک خط خواهد بود، همینطور که الان برای کامپایلر بدیهی است که inlining یک پیروزی در سایز کد و همچنین پرفورمنس (عملکرد) است. بررسی زمان اجرا و کد های جبران خسارت دیگر لازم نیستند: بیشتر کار های سنگین در زمان کامپایل انجام می‌شوند. ما دیگر به زمان اجرا و حافظه (رم) اضافی در سمت کلاینت نیاز نداریم.

امتحان کنید!

ما شما را تشویق می‌کنیم که null safety را امتحان کنید که در دارت 2.12 موجود است، حال در کانال بتای ما. بعد از انتقال (migrate) وابستگی (dependency) های بالادستی خود خواهید توانست که پکیج ها و برنامه های خود را انتقال دهید. همانطور که مثال اینجا نشان می‌دهد، شما ممکن است نیاز به تغییر خیلی زیادی نداشته باشید.

به یاد داشته باشید، برای گرفتن مزایای عملکردی null safety. وقتی که برنامه شما کاملا منتقل شود، کامپایلر های ما به شکل خودکار مزیت های null safety را برای تولید کد بهتر و کم حجم تر به کار می‌گیرند.

ضمیمه: کد

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

int N = 1000000; class Animal { int age = 0; } class Cat extends Animal { } class Dog extends Animal { } class Snake extends Animal { } class Hamster extends Animal { } List<Animal> _animals = [ new Cat()..age = 1, new Hamster()..age = 2, new Dog()..age = 3 ]; List<Animal> listOfA = [ ]; void init() { for (int i = 0; i < N; ++i) { listOfA.add(_animals[i % _animals.length]); } } int sum( ) { int k = 0; for (int i = 0; i < N; ++i) { k += getAge(listOfA[i]); } return k; } @pragma(&quotvm:never-inline&quot) int getAge(Animal a) { return a.age; } void main( ) { init(); print(sum()); print(getAge(listOfA[0])); }

منبع مقاله اصلی (سایت Medium) : Dart and the performance benefits of sound types

dartدارتtype systemsound typesnull safety
تا زمانی که علم تاییدش نکنه باورش نمی‌کنم، حتی اگر جلوی چشمام باشه!
شاید از این پست‌ها خوشتان بیاید