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

کریس ویلسون
Chris Wilson

مقدمه

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

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

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

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

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

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

بنابراین، در مورد نکات V8!

کلاس های پنهان

جاوا اسکریپت اطلاعات نوع زمان کامپایل محدودی دارد: انواع را می توان در زمان اجرا تغییر داد، بنابراین طبیعی است که انتظار داشته باشیم که استدلال در مورد انواع JS در زمان کامپایل گران باشد. این ممکن است شما را به این سوال سوق دهد که چگونه عملکرد جاوا اسکریپت می تواند به 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!```

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

بنابراین

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

اعداد

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

به عنوان مثال:

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

بنابراین

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

آرایه ها

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

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

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

بنابراین

  • برای آرایه ها از کلیدهای پیوسته استفاده کنید
  • آرایه های بزرگ (مانند عناصر > 64K) را از قبل به حداکثر اندازه آنها تخصیص ندهید، در عوض هر چه می خواهید رشد کنید
  • عناصر موجود در آرایه ها، به خصوص آرایه های عددی را حذف نکنید
  • عناصر اولیه یا حذف شده را بارگیری نکنید:
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] باعث می‌شود که آرایه به آرایه‌ای از دوبل‌های جعبه‌نشده تبدیل شود، اما سپس انتساب a[3] باعث می‌شود که دوباره آن را تغییر دهیم. دوباره به آرایه ای تبدیل می شود که می تواند هر مقداری (اعداد یا اشیاء) را داشته باشد. در حالت دوم، کامپایلر انواع همه عناصر موجود در لفظ را می‌داند و کلاس پنهان را می‌توان از قبل تعیین کرد.

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

کامپایل جاوا اسکریپت

اگرچه جاوا اسکریپت یک زبان بسیار پویا است و پیاده سازی های اصلی آن مفسر بودند، موتورهای زمان اجرا جاوا اسکریپت مدرن از کامپایل استفاده می کنند. V8 (جاوا اسکریپت کروم) دو کامپایلر مختلف Just-In-Time (JIT) دارد، در واقع:

  • کامپایلر "Full" که می تواند کد خوبی برای هر جاوا اسکریپت تولید کند
  • کامپایلر Optimizing که کدهای عالی برای اکثر جاوا اسکریپت تولید می کند، اما کامپایل آن زمان بیشتری می برد.

کامپایلر کامل

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

هدف Inline Caches این است که انواع را به طور موثر مدیریت کند، با ذخیره کد وابسته به نوع برای عملیات. هنگامی که کد اجرا می شود، ابتدا فرضیات نوع را تأیید می کند، سپس از کش درونی برای میانبر کردن عملیات استفاده می کند. با این حال، این بدان معنی است که عملیاتی که چندین نوع را می پذیرند، عملکرد کمتری خواهند داشت.

بنابراین

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

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

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

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

کامپایلر بهینه سازی

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

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

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

d8 --trace-opt primes.js

(این اسامی توابع بهینه شده را در stdout ثبت می کند.)

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

بنابراین

  • اگر بلوک‌های {} catch {} را امتحان کرده‌اید، کد حساس به perf را در یک تابع تودرتو قرار دهید: ```function js perf_sensitive() { // کار حساس به عملکرد را اینجا انجام دهید }

سعی کنید { perf_sensitive() } catch (e) { // در اینجا موارد استثنا را کنترل کنید } ```

این راهنما احتمالاً در آینده تغییر خواهد کرد، زیرا بلوک‌های 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 چگونه جاوا اسکریپت را اجرا می کند - اما مطمئن شوید که روی بهینه سازی الگوریتم های خود نیز تمرکز کنید!

مراجع