أساسيات جافا سكريبت Javascript: لماذا يجب أن تعرف كيفية عمل المحرك؟

لماذا يجب على مطوّر البرمجيات الذي يستخدم لغة جافا سكريبت Java Script، أن يعرف آلية عمل المحرّكات، حتى يكون تنفيذ الكود البرمجي بشكل صحيح؟

سوف نوضّح أهمية ما أسلفناه عن طريق طرح مسألة برمجية لنرى أنّه في دالّة من سطر واحد، تعيد الخاصية LastName للـمحدِّد argument المُدخل، بإضافة خاصية واحدة أُخرى لكل غرض object نحصل على أداء يتحسّن بنسبة قد تصل إلى أكثر من 700%!

كبح الفرامل عند السرعة القصوى

عادة، لا نكون بحاجة لمعرفة المكونات الداخلية للمحرك الذي ينفِّذ الكود البرمجي الخاص بنا. لكن، فليكن بالحسبان أنّ الشركاتِ، التي تُصنع وتبيع المتصفحات، تستثمر أموال طائلة في جعل محركات هذه المتصفحات أسرع!

رائع!

فلتدعْ الاّخرين يقومون بالعبء الثقيل، لماذا تزعج نفسك بالقلق حول كيفية عمل هذه المحركات؟

لأنك يمكن أن تكون مطوّر جافا سكريبت، ونحن يهمنا أن يكون عملك هو الأفضل، بالطبع! 😉

مثالُنا يقول:

يحتوي البرنامج على 5 أغراض، تخزن الأسماء الأولى والأخيرة لشخصيات فيلم Star Wars. تعيد الدالةُ (التابع) getName القيمةَ المخزنة في lastname. والمطلوب أن نقيس الزمن الكليّ الذي يستغرقه تنفيذ الدالة مليار مرة.

إذا كان المُعالج المُستخدم هو معالج Intel i7 4510U، فإنّ زمن التنفيذ سيكون 1.2 ثانية تقريباً. وبعد إضافة خاصية أخرى لكل غرض، بتنفيذ البرنامج مرة أخرى، كما يأتي:

نلاحظ أن زمن التنفيذ يزداد إلى 8.5 ثانية تقريباً، أي أصبح البرنامج أبطأ بسبع مرات من النسخة الأولى. يٌمكن تشبيه ذلك بالأثر الذي يخلّفه كبح الفرامل عندما تكون  السرعة قصوى. كيف يمكن لذلك أن يحدث؟

حان الوقت لأخذ نظرة أقرب عن المحرّك.

القوى المتحدة: المُفسّر Interpreter  والمترجم Compiler

المحرك هو الذي يقوم بقراءة وتنفيذ الرمّاز المصدر (الكود المصدري Source code)، وكل شركة مالكة لمُتصفح مُعين يكون لديها محركها الخاص. على سبيل المثال، متصفّح Mozilla Firefox مُحركه هو Spidermonkey، والمتصفّح Microsoft Edge مُحركه هو Chakra/chakraCore، ومتصفّح Apple Safari  محرّكه هو JavaScriptCore، أما متصفح Google Chrome يستخدم المُحرك V8،الذي يستخدمه Node.js كمُحرّك أيضاً.

لقد سجّل إطلاق المحرك V8  في سنة 2008 نقطةً محوريةً في تاريخ المحرّكات، حيث حسّن المحرك V8  عمليّةَ التفسير البطيء للكود البرمجي في لغة جافا سكريبت. إذ يكمن السبب وراء هذا التحسن الكبير، أساساً، في الدمج بين المٌفسِّر، والمُترجم. تستخدم، اليوم، جميعُ الأنواعِ الأربعةِ للمحركات هذه التقنية.

إنّ كل من المترجم والمُفسِّر، في واقع الأمر، عبارة عن برنامجين يقومان بنفس الغرض، والذي يتمثّل في تحويل الأوامر ( الكود البرمجي) المكتوبة بإحدى لغات البرمجة العالية المستوى ( كلغة جافا أو C++، وما إلى ذلك)، إلى لغة الآلة ( أي أوامر مكتوبة باللغة الثنائية؛ سلاسل من الأصفار والواحدات).

لكن يكمن الفرق الجوهري بينهما في الآتي:

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

بينما يقوم المُفسِّر بتحويل الكود المكتوب بلغة البرمجة إلى لغة وسيطة  intermediate code، بعدها، يحوّل كل جزء من أجزاء الكود الناتج إلى لغة الآلة، ومن ثم ينفّذ هذه الأجزاء الواحد تلو الآخر (Statement by statement)، وأي تعليمة تحوي خطأ تؤدي إلى إيقاف عملية التحويل بالنسبة لبقيّة الأجزاء.

أيضاً، عندما يقوم المترجم بتوليد الكود بلغة الآلة ، يقوم بتطبيق عمليات استمثال. لتعطي كلّ من عمليتي الترجمة والاستمثال، في النتيجة، تنفيذ أسرع للكود، بالرغم من الوقت الإضافي الذي يحتاجه طور الترجمة.

تكمن الفكرة الرئيسية وراء المحركات الحديثة، في الدمج بين أفضل الميزات الموجودة في كل من العمليتين التاليتين:

  • تنفيذ سريع للمُفسِّر.
  • تنفيذ سريع للمُترجِم.

يبدأ تحقيق كل من الهدفين بالمُفسِّر؛ على التوازي، تنفذ أعلام المحرك أجزاء من الكود، تكرارياً، بما يسمى بـ Hot Path، بعد ذلك، تُمرّر هذه الأجزاء إلى المُترجم مع المعلومات السياقية، التي يتم جمعها خلال التنفيذ، سامحةً  للمُترجِم بترجمة واستمثال الكود إلى السياق الحالي.

نسمي هذا السلوك للمترجم بـ”Just In Time”، أو باختصار JIT. إذ عندما يعمل المحرك بشكل جيد يمكنك تخيل سيناريوهات معينة حيث تتفوق جافا سكريبت على لغة الـسي بلس بلس. ولا عجب أن معظم عمل المحرك يكون في الاستمثال السياقي contextual optimization.

الأنواع الثابتة أثناء فترة التنفيذ:

تقنية التخبئة موضعية Inline Caching: IC

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

تعيّن تقنية التخبئة الموضعية لكل غرض نمط type ،يقوم المحرك بتوليده خلال فترة التنفيذ. يسميها V8 أنواع “types”، وهي ليست جزءاً من ال ECMAScript القياسية، أو الصفوف المخفية hidden class، أو أشكال الغرض object shapes. ولكي يتشارك غرضان بنفس الشكل shape، يجب أن يملكا الخصائص ذاتها، تماماً، وبنفس الترتيب. لذا غرض مثل :

{firstname : “Han”، lastname : “solo” }

يجب أن يعين إلى صفّ مختلف عن:

{lastname : “solo”، firstname : “Han” }

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

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

أما في التنفيذ الثاني، فإنّنا نتعامل مع 5  أشكال غرض مختلفة، وكل غرض لديه خاصية إضافية، و yoda ينقصه firstname كلياً. ماذا يحدث عندما نتعامل مع أشكال غرض متعددة multiple object shapes ؟

Ducks  أو الأنواع المتعددة

برمجة الدوال تملك ما يسمى بالمصطلح المعروف “duck typing” حيث يستدعي كود ذو صفات جيدة دوالاً يمكنها التعامل مع أنواع متعددة multiple types . وفي حالتنا، طالما أنّ الغرض المُمرّر يملك الخاصية lastname، سيكون كل شيء على ما يُرام .

تلغي خاصية التخبئة الموضعية عملية البحث المكلفة عن موقع الخاصية في الذاكرة. وهذا يعمل بشكل أفضل عندما يملك الغرض نفس الشكل في كل عملية وصول للخاصية. نُطلق على ذلك اسم التخبئة الموضعية وحيدة الشكل monomorphic IC. أما إذا كان لدينا ما يصل إلى أربعة أشكال غرض مختلفة، هنا نصبح في حالة التخبئة الموضعية متعددة الأشكال polymorphic IC .

كما في حالة التخبئة الموضعية وحيد الشكل، “يعرّف” الكود المحسن والمكتوب الاَلة جميع المواقع الأربعة مسبقاً. ولكن يجب عليه أن يعرف لأي من أشكال الغرض الأربعة المحتملة ينتمي المُحدّد  المُمرّر. هذا يسبب بالنتيجة انخفاض في الأداء. لكن عندما نتجاوز عتبة الأربعة أشكال، يزداد الوضع سوءاً. ونصبح في حالة التخبئة الموضعية متعددة الأشكال. في هذه الحالة لا يوجد أي تخزين مؤقت محلي لمواقع الذاكرة بعد الاَن. وعوضاً عن ذلك، يجب أن يتم البحث عنها في المخزن الشامل global cache.

هذا يعطي بالنتيجة انخفاض أداء عالي كما رأينا منذ قليل.

 تعدد الأشكال (Polymorphic) ووحيد الشكل(megamorphic)

نرى في المثال أدناه، تخزين مؤقت وحيد الشكل مع شكلين مختلفين للغرض.

والتخزين المؤقت وحيد الشكل من مثالنا مع 5 أشكال مختلفة للغرض.

صفّ جافا سكريبت للإنقاذ

حسناً، لدينا 5 أشكال للغرض، وتنفيذ في حالة التخبئة الموضعية متعددة الأشكال، كيف يمكننا إصلاح ذلك؟

علينا التأكد من أن المحرك يقوم بتحديد الأغراض الخمسة التي لدينا بشكل الغرض ذاته. هذا يعني بأن الأغراض التي وضعناها يجب أن تحوي كل الخاصيات الممكنة. لذا يمكننا استخدام الكائنات الحَرفية للغرض object literals، لكن نجد بأنّ الصفوف هي الحلّ الأفضل. وبالنسبة للخاصيات غير المصرّح عنها، يمكننا ببساطة  أن نمررnull، أو أن نتركها. إذ يتأكد الباني constructor من أن هذه الحقول مُهيأة بقيمة أولية لكلّ منها.

عندما نقوم بتنفيذ هذه الدالة مرة ثانية، نجد بأن زمن التنفيذ يكون 1.2 ثانية مرة أخرى.

الخلاصة:

تجمَع محرّكات لغة جافا سكريبت الحديثة سمات كل من المُترجم، والمُفسِّر، وبالتالي: إطلاق تطبيق سريع، وتنفيذ كود سريع. كما أنّ التخبئة الموضعية هي تقنية استمثال قوية، وتعمل بشكل أفضل عندما يكون شكل واحد للغرض مُمرّر إلى الدالة المُحسنة.

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

  • ترجمة: ديانا الضامن
  • مراجعة: نور عبدو
مصدر freecodecamp
تعليقات
Loading...

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More