نکته‌های عملکرد برای جاوا اسکریپت در V8

Chris Wilson
Chris Wilson

مقدمه

دنیل کلیفورد در Google I/O سخنرانی عالی درباره نکته‌ها و ترفندهای بهبود عملکرد جاوا اسکریپت در V8 ارائه داد. دانیل ما را تشویق کرد که «سرعت بیشتری را مطالبه کنیم» - تفاوت‌های عملکردی بین C++‎ و جاوا اسکریپت را به‌دقت تحلیل کنیم و کد را با درنظر گرفتن نحوه عملکرد جاوا اسکریپت بنویسیم. خلاصه‌ای از مهم‌ترین نکات صحبت‌های دانیل در این مقاله آورده شده است و ما نیز این مقاله را با تغییرات راهنمای عملکرد به‌روز نگه خواهیم داشت.

مهم‌ترین توصیه

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

بهترین توصیه پایه برای دریافت عملکرد خوب در برنامه‌های وب این است:

  • قبل‌از اینکه با مشکلی مواجه شوید (یا متوجه آن شوید) آماده باشید
  • سپس، نکته اصلی مشکلتان را شناسایی و درک کنید
  • درنهایت، آنچه را که مهم است اصلاح کنید

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

بنابراین، به نکته‌های V8 می‌پردازیم!

کلاس‌های پنهان‌شده

جاوا اسکریپت اطلاعات نوع زمان ترجمه محدودی دارد: انواع را می‌توان در زمان اجرا تغییر داد، بنابراین طبیعی است که انتظار داشته باشیم استدلال درباره انواع جاوا اسکریپت در زمان ترجمه پرهزینه باشد. این ممکن است باعث شود بپرسید که چگونه عملکرد جاوا اسکریپت می‌تواند به C++ نزدیک شود. بااین‌حال، V8 دارای انواع پنهانی است که به‌طور داخلی برای اشیاء در زمان اجرا ایجاد شده‌اند؛ اشیاء با کلاس پنهان یکسان می‌توانند از همان کد تولیدشده بهینه‌سازی‌شده استفاده کنند.

برای مثال:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```

تا زمانی که عضو اضافی «.z» به نمونه شیء p2 اضافه نشود، p1 و p2 به‌طور داخلی کلاس پنهان یکسانی دارند - بنابراین V8 می‌تواند یک نسخه واحد از اسمبلی بهینه‌شده را برای کد جاوا اسکریپت تولید کند که p1 یا p2 را دستکاری می‌کند. هرچه بیشتر بتوانید از واگرایی کلاس‌های پنهان جلوگیری کنید، عملکرد بهتری خواهید داشت.

علامت درنتیجه

  • همه اعضای شیء را در توابع سازنده مقداردهی اولیه کنید (تا نمونه‌ها بعداً نوع را تغییر ندهند)
  • همیشه اعضای شیء را با همان ترتیب مقداردهی اولیه کنید

Numbers

V8 از برچسب‌گذاری برای نمایش کارآمد مقادیر در زمانی که انواع می‌توانند تغییر کنند استفاده می‌کند. V8 از مقادیری که استفاده می‌کنید استنباط می‌کند که با چه نوع عددی سروکار دارید. وقتی V8 این استنتاج را انجام داد، از برچسب‌گذاری برای نمایش کارآمد مقادیر استفاده می‌کند، زیرا این نوع‌ها می‌توانند به‌صورت پویا تغییر کنند. بااین‌حال، گاهی اوقات تغییر این برچسب‌های نوع هزینه دارد، بنابراین بهتر است از انواع اعداد به‌طور مداوم استفاده کنید و به‌طور کلی، بهینه‌ترین حالت استفاده از اعداد صحیح علامت‌دار ۳۱ بیتی درصورت لزوم است.

برای مثال:

var i = 42;  // this is a 31-bit signed integer
var j = 4.2;  // this is a double-precision floating point number```

علامت درنتیجه

  • مقادیر عددی را ترجیح دهید که می‌توانند به‌عنوان اعداد صحیح علامت‌دار ۳۱ بیتی نشان داده شوند.

آرایه‌ها

برای مدیریت آرایه‌های بزرگ و پراکنده، دو نوع ذخیره‌سازی آرایه به‌صورت داخلی وجود دارد:

  • Fast Elements: فضای ذخیره‌سازی خطی برای مجموعه‌های کلید فشرده
  • عناصر واژه‌نامه: درغیراین‌صورت، ذخیره‌سازی جدول درهم‌سازی

بهتر است باعث نشوید که فضای ذخیره‌سازی آرایه از یک نوع به نوع دیگر تغییر کند.

علامت درنتیجه

  • برای آرایه‌ها، از کلیدهای مجاور که از ۰ شروع می‌شوند استفاده کنید
  • «آرایه‌های» بزرگ (مثلاً بیشتر از ۶۴ هزار عنصر) را ازقبل به حداکثر اندازه اختصاص ندهید، بلکه به‌تدریج آن‌ها را افزایش دهید
  • عناصر را در آرایه‌ها، به‌ویژه آرایه‌های عددی، حذف نکنید
  • عناصر مقداردهی‌نشده یا حذف‌شده را بار نکنید:
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
}

همچنین، آرایه‌های اعداد اعشاری با دقت مضاعف سریع‌تر هستند - کلاس پنهان آرایه انواع عناصر را ردیابی می‌کند و آرایه‌هایی که فقط حاوی اعداد اعشاری با دقت مضاعف هستند، بدون جعبه هستند (که باعث تغییر کلاس پنهان می‌شود). بااین‌حال، دستکاری بی‌دقت آرایه‌ها می‌تواند به‌دلیل جعبه‌بندی و جعبه‌گشایی باعث کار اضافی شود - برای مثال.

var a = new Array();
a[0] = 77;   // Allocates
a[1] = 88;
a[2] = 0.5;   // Allocates, converts
a[3] = true; // Allocates, converts```

کارایی کمتری دارد نسبت به:

var a = [77, 88, 0.5, true];

زیرا در مثال اول، تخصیص‌های فردی یکی پس‌از دیگری انجام می‌شوند و تخصیص a[2] باعث می‌شود که «آرایه» به «آرایه» از مقادیر double بدون جعبه تبدیل شود، اما سپس تخصیص a[3] باعث می‌شود که دوباره به «آرایه‌ای» تبدیل شود که می‌تواند هر مقداری (اعداد یا اشیاء) را دربرگیرد. در حالت دوم، کامپایلر انواع همه عناصر را در لیترال می‌داند و کلاس پنهان را می‌توان ازقبل تعیین کرد.

  • مقداردهی اولیه بااستفاده از آرایه‌های لفظی برای آرایه‌های کوچک با اندازه ثابت
  • آرایه‌های کوچک (کمتر از ۶۴ کیلوبایت) را قبل‌از استفاده به اندازه صحیح پیش‌تخصیص دهید
  • مقادیر غیرعددی (اشیاء) را در آرایه‌های عددی ذخیره نکنید
  • مراقب باشید که اگر بدون لیترال مقداردهی اولیه می‌کنید، باعث تبدیل مجدد آرایه‌های کوچک نشوید.

گردآوری جاوا اسکریپت

اگرچه جاوا اسکریپت یک زبان بسیار پویا است و پیاده‌سازی‌های اصلی آن مفسر بودند، موتورهای زمان اجرای جاوا اسکریپت مدرن از کامپایل استفاده می‌کنند. V8 (جاوا اسکریپت Chrome) درواقع دو کامپایلر «درجا» (JIT) متفاوت دارد:

  • کامپایلر «کامل» که می‌تواند کد خوبی برای هر جاوا اسکریپت تولید کند
  • کامپایلر بهینه‌سازی که کد عالی برای اکثر جاوا اسکریپت‌ها تولید می‌کند، اما کامپایل آن بیشتر طول می‌کشد.

گردآورنده کامل

در V8، کامپایلر کامل روی همه کدها اجرا می‌شود و در اسرع وقت شروع به اجرای کد می‌کند و به‌سرعت کدی خوب اما نه عالی تولید می‌کند. این کامپایلر تقریباً هیچ فرضی درباره انواع در زمان کامپایل ندارد - انتظار دارد که انواع متغیرها بتوانند در زمان اجرا تغییر کنند و تغییر خواهند کرد. کد تولیدشده توسط کامپایلر Full از «حافظه‌های پنهان درون‌خطی» (IC) برای پالایش دانش درباره انواع درحین اجرای برنامه استفاده می‌کند و کارایی را در لحظه بهبود می‌بخشد.

هدف «حافظه نهان به‌خط» مدیریت کارآمد انواع با ذخیره کردن کد وابسته به نوع برای عملیات در حافظه نهان است؛ وقتی کد اجرا می‌شود، ابتدا فرض‌های نوع را اعتبارسنجی می‌کند، سپس از حافظه نهان به‌خط برای میان‌بر زدن عملیات استفاده می‌کند. بااین‌حال، این یعنی عملیاتی که چندین نوع را می‌پذیرد عملکرد کمتری خواهد داشت.

علامت درنتیجه

  • استفاده تک‌ریختی از عملیات بر عملیات چندریختی ترجیح داده می‌شود

اگر کلاس‌های پنهان ورودی‌ها همیشه یکسان باشند، عملیات تک‌ریخت است - درغیراین‌صورت چندریخت است، یعنی برخی‌از آرگومان‌ها می‌توانند نوع را در تماس‌های مختلف با عملیات تغییر دهند. برای مثال، دومین فراخوانی ()add در این مثال باعث چندریختی می‌شود:

function add(x, y) {
  return x + y;
}

add(1, 2);      // + in add is monomorphic
add("a", "b");  // + in add becomes polymorphic```

مترجم بهینه‌ساز

به‌موازات کامپایلر کامل، V8 توابع «داغ» (یعنی توابعی که بارها اجرا می‌شوند) را با کامپایلر بهینه‌سازی مجدداً کامپایل می‌کند. این کامپایلر از بازخورد نوع برای سریع‌تر کردن کد کامپایل‌شده استفاده می‌کند - درواقع، از انواع گرفته‌شده از ICهایی که درباره‌شان صحبت کردیم استفاده می‌کند!

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

بااستفاده از نسخه مستقل «d8» موتور V8 می‌توانید آنچه را بهینه‌سازی می‌شود ثبت کنید:

d8 --trace-opt primes.js

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

بااین‌حال، همه توابع را نمی‌توان بهینه‌سازی کرد - برخی‌از ویژگی‌ها مانع از اجرای کامپایلر بهینه‌سازی روی یک تابع خاص می‌شوند («bail-out»). به‌ویژه، کامپایلر بهینه‌سازی درحال‌حاضر از توابع با بلوک‌های try {} catch {} خارج می‌شود!

علامت درنتیجه

  • اگر بلوک‌های try {} catch {} دارید، کد حساس به عملکرد را در یک تابع تودرتو قرار دهید: ```js function perf_sensitive() { // Do performance-sensitive work here }

try { perf_sensitive() } catch (e) { // Handle exceptions here } ```

این راهنمایی احتمالاً در آینده تغییر خواهد کرد، زیرا ما بلوک‌های try/catch را در کامپایلر بهینه‌سازی فعال می‌کنیم. می‌توانید بااستفاده از گزینه «--trace-opt» با d8 (همان‌طور که در بالا نشان داده شده است) بررسی کنید که کامپایلر بهینه‌سازی چگونه از توابع خارج می‌شود. این گزینه اطلاعات بیشتری درباره توابعی که از آن‌ها خارج شده است ارائه می‌دهد:

d8 --trace-opt primes.js

غیربهینه‌سازی

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

علامت درنتیجه

  • از تغییرات کلاس پنهان در توابع پس‌از بهینه‌سازی آن‌ها اجتناب کنید

مانند سایر بهینه‌سازی‌ها، می‌توانید بااستفاده از پرچم گزارش‌گیری، گزارشی از توابعی که V8 مجبور به غیربهینه‌سازی آن‌ها شده است دریافت کنید:

d8 --trace-deopt primes.js

دیگر ابزارهای V8

ضمناً، می‌توانید گزینه‌های ردیابی V8 را نیز هنگام راه‌اندازی به Chrome ارسال کنید:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```

علاوه‌بر استفاده از نمایه‌سازی ابزارهای توسعه‌دهنده، می‌توانید از d8 نیز برای نمایه‌سازی استفاده کنید:

% out/ia32.release/d8 primes.js --prof

این کار از تحلیلگر نمونه‌گیری داخلی استفاده می‌کند که هر میلی‌ثانیه یک نمونه می‌گیرد و v8.log را می‌نویسد.

به‌طور خلاصه

مهم است که نحوه کار موتور V8 با کدتان را شناسایی و درک کنید تا برای ساختن جاوا اسکریپت با عملکرد خوب آماده شوید. یک‌بار دیگر، توصیه اصلی این است:

  • قبل‌از اینکه با مشکلی مواجه شوید (یا متوجه آن شوید) آماده باشید
  • سپس، نکته اصلی مشکلتان را شناسایی و درک کنید
  • درنهایت، آنچه را که مهم است اصلاح کنید

این یعنی باید مطمئن شوید که مشکل در جاوا اسکریپت شما است، با استفاده از ابزارهای دیگر مانند PageSpeed ابتدا؛ احتمالاً کاهش به جاوا اسکریپت خالص (بدون DOM) قبل از جمع‌آوری معیارها، و سپس استفاده از آن معیارها برای یافتن گلوگاه‌ها و حذف موارد مهم. امیدواریم صحبت‌های دانیل (و این مقاله) به شما کمک کند تا بهتر بفهمید که V8 چگونه جاوا اسکریپت را اجرا می‌کند - اما حتماً روی بهینه‌سازی الگوریتم‌های خود نیز تمرکز کنید!

مرجع‌ها