نصائح الأداء لـ JavaScript في الإصدار 8

Chris Wilson
Chris Wilson

مقدمة

ألقى "دانيال كليفورد" محادثة ممتازة في مؤتمر Google I/O حول نصائح لتحسين أداء JavaScript في الإصدار الثامن. لقد شجعنا دانيال على "الطلب بشكل أسرع" - لتحليل الاختلافات في الأداء بين C++ وJavaScript، وكتابة التعليمات البرمجية بعناية حول كيفية عمل JavaScript. تم تدوين ملخص لأهم نقاط خطاب دانيال في هذه المقالة، وسنُبقي هذه المقالة محدَّثة أيضًا مع حدوث تغييرات في إرشادات الأداء.

أهم نصيحة

من المهم وضع أي نصيحة حول الأداء في سياقها. لا شكّ في أنّ نصائح الأداء تسبب الإدمان، وقد يؤدي التركيز على النصائح العميقة أولاً إلى تشتيت الانتباه عن المشاكل الحقيقية. تحتاج إلى إلقاء نظرة شاملة على أداء تطبيق الويب، وقبل التركيز على هذه النصائح المتعلّقة بالأداء، من الأفضل تحليل الرمز باستخدام أدوات مثل PageSpeed وزيادة نتيجتك. وسيساعدك هذا في تجنُّب التحسين المبكر.

أفضل نصيحة أساسية للحصول على أداء جيد في تطبيقات الويب هي:

  • كن مستعدًا قبل أن تواجه (أو تلاحظ) مشكلة
  • بعد ذلك، حدد أهمّ ما في مشكلتك وافهمه
  • أخيرًا، أصلِح المشاكل

ولتنفيذ هذه الخطوات، قد يكون من المهم فهم كيف يحسّن V8 JavaScript، بحيث يمكنك كتابة التعليمات البرمجية مع مراعاة تصميم وقت تشغيل JavaScript. من المهم أيضًا التعرف على الأدوات المتاحة وكيف يمكن أن تساعدك. يقدم "دانيال" شرحًا إضافيًا حول كيفية استخدام أدوات المطوّرين في محادثته: يوضح هذا المستند بعض أهم النقاط في تصميم المحرّك V8.

حسنًا، إلى نصائح V8!

الصفوف المخفية

تحتوي JavaScript على معلومات محدودة عن وقت التجميع: يمكن تغيير الأنواع في وقت التشغيل، لذا من الطبيعي توقُّع أن تكون أنواع JavaScript في وقت التجميع مكلفًا. قد يقودك هذا إلى التساؤل عن كيفية اقتراب أداء JavaScript من أي مكان في 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." للكائن p1، يكون لدى p1 وp2 نفس الفئة المخفية داخليًا، ولذلك يمكن لـ V8 إنشاء نسخة واحدة من التجميع المحسّن لرمز JavaScript الذي يعالج إما 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 بت.

الصفائف

للتعامل مع الصفائف الكبيرة والمتناثرة، هناك نوعان من تخزين الصفائف داخليًا:

  • العناصر السريعة: تخزين خطي لمجموعات المفاتيح المدمجة
  • عناصر القاموس: تجزئة جدول التخزين

من الأفضل عدم نقل مساحة تخزين الصفيفة من نوع إلى آخر.

ولذلك

  • استخدام المفاتيح المتجاورة التي تبدأ من 0 للمصفوفات
  • لا تُخصص مسبقًا الصفائف الكبيرة (على سبيل المثال، > عناصر 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] يؤدي إلى إعادة تحويلها إلى مصفوفة يمكن أن تحتوي على أي قيم (أرقام أو كائنات). في الحالة الثانية، يعرف المحول البرمجي أنواع جميع العناصر في الحرفية، ويمكن تحديد الفئة المخفية مقدمًا.

  • التهيئة باستخدام القيم الحرفية للصفيفة للمصفوفات الصغيرة ذات الحجم الثابت
  • تخصيص الصفائف الصغيرة (<64 كيلوبايت) لتصحيح الحجم قبل استخدامها
  • عدم تخزين القيم غير الرقمية (الكائنات) في مصفوفات رقمية
  • احرص على عدم التسبب في إعادة تحويل الصفائف الصغيرة إذا تمت التهيئة بدون القيم الحرفية.

تجميع JavaScript

على الرغم من أنّ لغة JavaScript هي لغة ديناميكية للغاية، وقد كانت تطبيقاتها الأصلية تتيح الترجمة الفورية، إلا أنّ محرّكات وقت تشغيل JavaScript الحديثة تستخدم التجميع. يحتوي الإصدار V8 (في JavaScript من Chrome) على برنامجَي تجميع مختلفَين في الوقت الفعلي (JIT):

  • المحول البرمجي "الكامل"، الذي يمكنه إنشاء رمز جيد لأي برنامج JavaScript
  • المحول البرمجي لتحسين الأداء، الذي ينتج عنه تعليمات برمجية رائعة لمعظم لغة JavaScript، ولكن يستغرق وقتًا أطول في التجميع.

برنامج التحويل الكامل

وفي الإصدار V8، يعمل المحول البرمجي الكامل على جميع التعليمات البرمجية، ويبدأ في تنفيذ التعليمات البرمجية في أقرب وقت ممكن، مما يؤدي سريعًا إلى إنشاء رمز جيد وليس رائعًا. لا يفترض المحول البرمجي هذا أي شيء تقريبًا عن الأنواع في وقت التجميع - ويتوقع أن أنواع المتغيرات يمكن أن تتغير أو تتغير في وقت التشغيل. يستخدم الكود الذي يُنشئه المحول البرمجي الكامل ذاكرات التخزين المؤقت المضمنة (ICs) لتحسين المعرفة حول الأنواع أثناء تشغيل البرنامج، مما يؤدي إلى تحسين الكفاءة بسرعة.

الهدف من ذاكرات التخزين المؤقت المضمّنة هو معالجة الأنواع بكفاءة، عن طريق التخزين المؤقت للرمز الذي يعتمد على النوع للعمليات. وعند تشغيل الرمز، سيتم التحقّق من صحة افتراضات النوع أولاً، ثم استخدام ذاكرة التخزين المؤقت المضمّنة لاختصار العملية. غير أن هذا يعني أن العمليات التي تقبل العديد من الأنواع ستكون أقل أداءً.

ولذلك

  • يُفضل الاستخدام الأحادي للعمليات على العمليات متعددة الأشكال

تكون العمليات أحادية الشكل إذا كانت الفئات المخفية للمدخلات دائمًا هي نفسها، وبخلاف ذلك، تكون متعددة الأشكال، أي أن بعض الوسيطات يمكن أن تغير نوعها عبر استدعاءات مختلفة للعملية. على سبيل المثال، يتسبب استدعاء add() الثاني في هذا المثال في تعدد الأشكال:

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

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

أداة تجميع التحسين

بالتوازي مع برنامج التحويل البرمجي الكامل، يقوم V8 بإعادة تجميع دوال التشغيل "hot" (أي الدوال التي يتم تشغيلها عدة مرات) باستخدام برنامج تجميع محسّن. يستخدم المحول البرمجي هذا النوع الملاحظات لجعل التعليمة البرمجية المجمعة أسرع - في الواقع، يستخدم الأنواع المأخوذة من IC التي تحدثنا عنها للتو!

في برنامج تجميع العمليات البرمجي المحسَّن، يتم تضمين العمليات بشكل مبني على توقُّع (يتم وضعها مباشرةً في المكان الذي يتم استدعاؤها فيها). ويؤدي ذلك إلى تسريع عملية التنفيذ (على حساب استهلاك الذاكرة)، كما أنّها تتيح أيضًا إمكانية إجراء تحسينات أخرى. يمكن تضمين الدوال الأحادي الشكل بالكامل (هذا سبب آخر يجعل أحادي الشكل فكرة جيدة في V8).

يمكنك تسجيل البيانات التي يتم تحسينها باستخدام الإصدار المستقل "d8" من المحرك V8:

d8 --trace-opt primes.js

(يسجّل هذا أسماء الدوال المحسَّنة للإخراج القياسي).

لا يمكن تحسين جميع الدوال، إلا أن بعض الميزات تمنع تشغيل برنامج التجميع البرمجي المحسّن على وظيفة معينة ("عملية تعويض"). على وجه الخصوص، يستفيد حاليًا برنامج التحويل البرمجي المحسّن في الدوال التي تتضمن تجربة {} track {} من الكتل!

ولذلك

  • ضع رمزًا حساسًا للأداء في دالة متداخلة إذا كنت قد جرّبت {} track {} block: ```js function perf_sensitive() { // Do Performance-sensitive work هنا }

جرّب { perf_sensitive() }رس (e) { // معالجة الاستثناءات هنا } ```

ومن المحتمل أن تتغيّر هذه الإرشادات في المستقبل، لأنّنا نفعّل قوالب التجربة/الالتقاط في برنامج التحويل البرمجي المحسّن. يمكنك فحص كيفية استفادة برنامج التحويل البرمجي المحسَّن من الدوال باستخدام الخيار "--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 مع التعليمات البرمجية الخاصة بك للاستعداد لإنشاء لغة JavaScript فعالة. مرة أخرى، النصيحة الأساسية هي:

  • كن مستعدًا قبل أن تواجه (أو تلاحظ) مشكلة
  • بعد ذلك، حدد أهمّ ما في مشكلتك وافهمه
  • أخيرًا، أصلِح المشاكل

وهذا يعني أنّه عليك التأكّد من أنّ المشكلة تكمن في JavaScript باستخدام أدوات أخرى مثل PageSpeed أولاً، وقد يتم التقليل من استخدام JavaScript فقط (بدون نموذج DOM) قبل جمع المقاييس، ثم استخدام هذه المقاييس لتحديد المؤثّرات السلبية وإزالة العوامل المهمة. نأمل أن يساعدك محاضرة دانيال (وهذه المقالة) في فهم كيفية تشغيل V8 لـ JavaScript بشكل أفضل، ولكن تأكد من التركيز على تحسين الخوارزميات الخاصة بك أيضًا!

المراجع