paint-brush
لماذا يعتبر تقييم أداء JavaScript فوضويًا؟بواسطة@asyncbanana
تاريخ جديد

لماذا يعتبر تقييم أداء JavaScript فوضويًا؟

بواسطة AsyncBanana7m2025/01/04
Read on Terminal Reader

طويل جدا؛ ليقرأ

لا يزال من الضروري إجراء معايرة لـ JavaScript، خاصة وأن JavaScript يستخدم في تطبيقات أكثر حساسية للأداء. تبذل محركات JavaScript قصارى جهدها للتخفيف من هجمات التوقيت، جنبًا إلى جنب مع التحرك للانتقال إلى [عدم التعقب] تعمل محركات JavaScript عمدًا على جعل قياسات التوقيت غير دقيقة.
featured image - لماذا يعتبر تقييم أداء JavaScript فوضويًا؟
AsyncBanana HackerNoon profile picture
0-item
1-item

أكره معايرة الكود، تمامًا مثل أي إنسان (والذي في هذه المرحلة، ربما لا يكون معظم مشاهدي هذا البرنامج من النوع ¯\ (ツ) /¯). من الممتع أكثر أن تتظاهر بأن تخزين قيمة معينة في ذاكرة التخزين المؤقت يزيد الأداء بنسبة 1000% بدلًا من اختبارها لمعرفة ما فعلته. للأسف، لا يزال معايرة الأداء في JavaScript ضروريًا، خاصة وأن JavaScript يستخدم ( عندما لا ينبغي أن يكون كذلك؟ ) في تطبيقات أكثر حساسية للأداء. لسوء الحظ، نظرًا للعديد من قراراته المعمارية الأساسية، لا تجعل JavaScript معايرة الأداء أسهل.

ما هو الخطأ في جافا سكريبت؟

يؤدي مُجمِّع JIT إلى تقليل الدقة (?)

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


يقدم كل من هذه المجمِّعات مقايضة مختلفة بين وقت التجميع وأداء وقت التشغيل، لذا لا يحتاج المستخدم إلى قضاء وقت الحوسبة في تحسين الكود الذي نادرًا ما يتم تشغيله مع الاستفادة من مزايا أداء المجمِّع الأكثر تقدمًا للكود الذي يتم تشغيله في أغلب الأحيان (المسارات الساخنة). هناك أيضًا بعض التعقيدات الأخرى التي تنشأ عند استخدام المجمِّعات المحسنة التي تتضمن كلمات برمجة معقدة مثل " أحادية الشكل الوظيفي "، لكنني سأوفر عليك ذلك وأتجنب الحديث عن ذلك هنا.


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


على سبيل المثال، في إعدادات القياس الأساسية لديك (لا تستخدم أي شيء مثل ما يلي لأسباب متعددة):

 for (int i = 0; i<1000; i++) { console.time() // do some expensive work console.timeEnd() }

(لا تقلق، سوف نتحدث عن console.time أيضًا)


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


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


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

تبذل المحركات قصارى جهدها لمنعك من ضبط التوقيت بدقة

هل تعلم ما هي تقنية بصمة الإصبع؟ ما هي التقنية التي سمحت باستخدام خاصية عدم التتبع للمساعدة في التتبع ؟ نعم، لقد بذلت محركات JavaScript قصارى جهدها للتخفيف من حدتها. وقد أدى هذا الجهد، إلى جانب التحرك لمنع هجمات التوقيت ، إلى قيام محركات JavaScript عمدًا بجعل التوقيت غير دقيق، وبالتالي لا يستطيع المتسللون الحصول على قياسات دقيقة لأداء الكمبيوتر الحالي أو مدى تكلفة عملية معينة.


لسوء الحظ، هذا يعني أنه بدون إجراء أي تعديلات، ستواجه معايير الأداء نفس المشكلة.


سيكون المثال الموجود في القسم السابق غير دقيق، لأنه لا يتم قياسه إلا بالمللي ثانية. الآن، قم بتبديله إلى performance.now() . رائع.


الآن، أصبح لدينا طوابع زمنية بالمايكروثانية!

 // Bad console.time(); // work console.timeEnd(); // Better? const t = performance.now(); // work console.log(performance.now() - t);

باستثناء... أنها كلها بزيادات مقدارها 100μs. الآن، دعنا نضيف بعض الرؤوس للتخفيف من خطر هجمات التوقيت. أوه، لا يزال بإمكاننا فقط زيادة 5μs. ربما تكون 5μs دقة كافية للعديد من حالات الاستخدام، ولكن سيتعين عليك البحث في مكان آخر عن أي شيء يتطلب المزيد من التفاصيل. بقدر ما أعلم، لا يسمح أي متصفح بمؤقتات أكثر تفصيلاً. Node.js يسمح بذلك، ولكن بالطبع، هذا له مشكلاته الخاصة.


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

كل بيئة مختلفة

أحب Bun لما فعلته لدفع JavaScript من جانب الخادم إلى الأمام، ولكن يا للهول، إنها تجعل معايرة JavaScript للخوادم أكثر صعوبة. قبل بضع سنوات، كانت بيئات JavaScript الوحيدة من جانب الخادم التي يهتم بها الناس هي Node.js و Deno ، وكلاهما يستخدم محرك JavaScript V8 (نفس المحرك في Chrome). يستخدم Bun بدلاً من ذلك JavaScriptCore، المحرك في Safari، والذي يتميز بخصائص أداء مختلفة تمامًا.


إن مشكلة وجود بيئات JavaScript متعددة بخصائص أداء خاصة بها جديدة نسبيًا في JavaScript من جانب الخادم ولكنها أزعجت العملاء لفترة طويلة. يمكن للمحركات الثلاثة الشائعة الاستخدام في JavaScript، V8 وJSC وSpiderMonkey لمتصفحات Chrome وSafari وFirefox، على التوالي، أن تعمل بشكل أسرع أو أبطأ بشكل ملحوظ على قطعة مكافئة من التعليمات البرمجية.


أحد الأمثلة على هذه الاختلافات هو تحسين استدعاء الذيل (TCO). يعمل تحسين استدعاء الذيل على تحسين الوظائف التي تتكرر في نهاية نصها، مثل هذا:

 function factorial(i, num = 1) { if (i == 1) return num; num *= i; i--; return factorial(i, num); }


حاول إجراء معايرة factorial(100000) في Bun. الآن، حاول نفس الشيء في Node.js أو Deno. يجب أن تحصل على خطأ مشابه لهذا:

 function factorial(i, num = 1) { ^ RangeError: Maximum call stack size exceeded


في الإصدار V8 (وبالتالي Node.js وDeno)، في كل مرة تستدعي فيها الدالة factorial() نفسها في النهاية، ينشئ المحرك سياق وظيفة جديد تمامًا لتشغيل الوظيفة المتداخلة، والذي يقتصر في النهاية على مكدس النداء. ولكن لماذا لا يحدث هذا في Bun؟ JavaScriptCore، الذي يستخدمه Bun، ينفذ TCO، الذي يعمل على تحسين هذه الأنواع من الوظائف عن طريق تحويلها إلى حلقة for أكثر مثل هذا:

 function factorial(i, num = 1) { while (i != 1) { num *= i; i--; } return i; }

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


في الأساس، تعني هذه الاختلافات أنه يجب عليك إجراء معايرة عبر جميع المحركات التي تتوقع تشغيل الكود الخاص بك عليها للتأكد من أن الكود الذي يكون سريعًا في أحد المحركات لا يكون بطيئًا في محرك آخر. أيضًا، إذا كنت تقوم بتطوير مكتبة تتوقع استخدامها عبر العديد من المنصات، فتأكد من تضمين محركات أكثر تعقيدًا مثل Hermes ؛ فهي تتمتع بخصائص أداء مختلفة تمامًا.

إشارات شرفية

  • جامع القمامة وميله إلى إيقاف كل شيء بشكل عشوائي.


  • قدرة مُجمِّع JIT على حذف كافة التعليمات البرمجية الخاصة بك لأنها "غير ضرورية".


  • رسوم بيانية لهب واسعة للغاية في معظم أدوات تطوير JavaScript.


  • أعتقد أنك حصلت على النقطة.

إذن… ما هو الحل؟

أتمنى أن أتمكن من الإشارة إلى حزمة npm التي تحل كل هذه المشكلات، ولكن في الواقع لا يوجد واحدة.


على الخادم، سيكون الأمر أسهل قليلاً. يمكنك استخدام d8 للتحكم يدويًا في مستويات التحسين، والتحكم في جامع القمامة، والحصول على توقيت دقيق. بالطبع، ستحتاج إلى بعض Bash-fu لإعداد خط أنابيب معياري مصمم جيدًا لهذا الغرض، حيث لسوء الحظ، فإن d8 غير متكامل جيدًا (أو غير متكامل على الإطلاق) مع Node.js.


يمكنك أيضًا تمكين علامات معينة في Node.js للحصول على نتائج مماثلة، ولكنك ستفقد ميزات مثل تمكين مستويات التحسين المحددة.

 v8 --sparkplug --always-sparkplug --no-opt [file]

مثال على D8 مع تمكين طبقة تجميع محددة (sparkplug). يتضمن D8، بشكل افتراضي، مزيدًا من التحكم في GC ومزيدًا من معلومات التصحيح بشكل عام.


يمكنك الحصول على بعض الميزات المشابهة على JavaScriptCore؟؟؟ بصراحة، لم أستخدم واجهة سطر الأوامر الخاصة بـ JavaScriptCore كثيرًا، وهي غير موثقة بشكل كبير . يمكنك تمكين طبقات معينة باستخدام علامات سطر الأوامر الخاصة بها ، لكنني لست متأكدًا من مقدار معلومات التصحيح التي يمكنك استردادها. يتضمن Bun أيضًا بعض أدوات القياس المعيارية المفيدة، لكنها محدودة بشكل مشابه لـ Node.js.


لسوء الحظ، يتطلب كل هذا محرك الأساس/إصدار الاختبار للمحرك، والذي قد يكون من الصعب الحصول عليه. لقد وجدت أن أبسط طريقة لإدارة المحركات هي استخدام esvu مع eshost-cli ، حيث يجعلان معًا إدارة المحركات وتشغيل التعليمات البرمجية عبرها أسهل بكثير. بالطبع، لا يزال هناك الكثير من العمل اليدوي المطلوب، حيث تدير هذه الأدوات تشغيل التعليمات البرمجية عبر محركات مختلفة فقط - لا يزال يتعين عليك كتابة التعليمات البرمجية المعيارية بنفسك.


إذا كنت تحاول فقط معايرة محرك باستخدام الخيارات الافتراضية بأكبر قدر ممكن من الدقة على الخادم، فهناك أدوات Node.js جاهزة مثل mitata التي تساعد في تحسين دقة التوقيت والأخطاء المتعلقة بـ GC. يمكن أيضًا استخدام العديد من هذه الأدوات، مثل Mitata، عبر العديد من المحركات؛ بالطبع، لا يزال يتعين عليك إعداد خط أنابيب مثل ما سبق.


على المتصفح، كل شيء أكثر صعوبة. لا أعرف أي حلول لتوقيت أكثر دقة، والتحكم في المحرك أكثر محدودية. معظم المعلومات التي يمكنك الحصول عليها فيما يتعلق بأداء JavaScript وقت التشغيل في المتصفح ستكون من أدوات تطوير Chrome ، والتي تقدم أدوات أساسية لمحاكاة الرسم البياني للهب وتباطؤ وحدة المعالجة المركزية.

خاتمة

إن العديد من قرارات التصميم نفسها التي جعلت JavaScript (نسبيًا) عالية الأداء وقابلة للنقل تجعل عملية المعايرة أكثر صعوبة بشكل كبير مقارنة باللغات الأخرى. فهناك العديد من الأهداف التي يجب معايرة أدائها، ولديك سيطرة أقل كثيرًا على كل هدف.


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


بغض النظر عن الطريقة التي تختارها لمعايرة الأداء، آمل أن توضح لك هذه المقالة بعض المشكلات الموجودة في معايرة الأداء في JavaScript. أخبرني إذا كان من المفيد تقديم برنامج تعليمي حول تنفيذ بعض الأشياء التي وصفتها أعلاه.