مقدمة
ألقى "دانيال كليفورد" محاضرة ممتازة في مؤتمر Google I/O حول نصائح لتحسين أداء JavaScript في الإصدار 8. شجّعنا دانيال على "الطلب بشكل أسرع" - لتحليل الاختلافات في الأداء بين C++ وJavaScript بعناية، وكتابة الرمز بعناية مع فهم طريقة عمل JavaScript. تم الحصول على ملخص لأهم النقاط في حديث دانيال في هذه المقالة، وسنحرص أيضًا على تحديث هذه المقالة كلما طرأت تغييرات على إرشادات الأداء.
أهم نصيحة
من المهم وضع أي نصيحة متعلقة بالأداء في سياقها. لا شك أن نصيحة الأداء تسبب إدمانًا، وقد يصرف التركيز أحيانًا على النصيحة العميقة عن المشكلات الحقيقية. عليك إلقاء نظرة شاملة على أداء تطبيق الويب، وقبل التركيز على نصيحة الأداء هذه، من الأفضل تحليل الرمز باستخدام أدوات مثل PageSpeed وتحسين نتيجتك. حيث سيساعدك هذا على تجنب التحسين المبكر.
أفضل نصيحة أساسية للحصول على أداء جيد في تطبيقات الويب هي:
- الاستعداد قبل أن تواجه (أو تلاحظ) مشكلة
- بعد ذلك، حدد وافهم ركيزة مشكلتك
- أخيرًا، إصلاح ما يهم
لتنفيذ هذه الخطوات، قد يكون من المهم فهم كيفية تحسين V8 لـ JS، وبالتالي يمكنك كتابة التعليمات البرمجية مع مراعاة تصميم وقت تشغيل JS. من المهم أيضًا معرفة الأدوات المتاحة وكيف يمكنها مساعدتك. في حديثه، يقدّم دانيال المزيد من الشرح حول كيفية استخدام أدوات المطوّرين. فإن هذا المستند يتناول بعض أهم النقاط في تصميم محرك 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!```
إلى أن يحتوي مثيل الكائن p2 على عضو إضافي "z." تكون p1 وp2 داخلهما نفس الفئة المخفية، وبالتالي يمكن لـ V8 إنشاء نسخة واحدة من التجميع المحسّن لرمز JavaScript الذي يعالج إما p1 أو p2. وكلما تمكنت من تجنب التسبب في تباعد الفئات المخفية، تحسَّن الأداء الذي ستحصل عليه.
لذلك
- تهيئة جميع أعضاء الكائن في الدوال الإنشائية (حتى لا يتغير نوع المثيلات لاحقًا)
- إعداد أعضاء الكائن دائمًا بالترتيب نفسه
Numbers
يستخدم 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 للصفائف
- لا تخصِّص مسبقًا صفائف كبيرة (مثلاً > 64 ألف عنصر) إلى أقصى حجم لها، بل يزداد عددها تدريجيًا.
- لا تحذف العناصر في المصفوفات، وخاصة المصفوفات الرقمية
- لا تحمِّل عناصر محذوفة أو لم يتم إعدادها:
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
(يسجّل هذا أسماء الدوال المحسَّنة إلى stdout.)
ومع ذلك، لا يمكن تحسين جميع الدوال، حيث تمنع بعض الميزات تشغيل المجمِّع التحسيني من خلال دالة معينة (عملية "تجاوز"). على وجه الخصوص، ينتقل المحول البرمجي المحسّن حاليًا إلى الدوال مع تجربة {} الكتل البرمجية {}!
لذلك
- ضع التعليمة البرمجية الحساسة للأداء في دالة متداخلة إذا جرّبت {} الكتل البرمجية {}: ```js function perf_sensitive() { // إجراء عمل حساس للأداء هنا }
جرّب { perf_sensitive() } catch (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، ولكن احرص أيضًا على التركيز على تحسين خوارزمياتك الخاصة.