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

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

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

التالي هو خط أنابيب يوضح كيفية عمل V8 ، محرك JavaScript المستخدم من قبل Chrome و Node.js.

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

على سبيل المثال ، يعمل محرك SpiderMonkey JavaScript الخاص بـ Mozilla ، والذي يستخدم في Firefox و
SpiderNode ، بشكل مختلف قليلاً. انها ليست واحدة ، ولكن اثنين من المجمعين الأمثل. تم تحسين المترجم الشفوي إلى مترجم أساسي (مترجم Baseline) ، والذي ينتج عنه بعض التعليمات البرمجية المحسنة. جنبا إلى جنب مع البيانات الشخصية التي تم جمعها أثناء تنفيذ التعليمات البرمجية ، يمكن لبرنامج التحويل البرمجي IonMonkey إنشاء رمز مُحسّن بشكل كبير. إذا فشل تحسين المضاربة ، يعود IonMonkey إلى رمز Baseline.

Chakra - يحتوي محرك جافا سكريبت من Microsoft ، والمستخدم في Edge و
Node-ChakraCore ، على بنية متشابهة للغاية ويستخدم اثنين
من المترجمين المحسنين . تم تحسين المترجم الفوري في SimpleJIT (حيث يرمز JIT إلى "مترجم Just-In-Time" ، والذي ينتج رمزًا محسّنًا إلى حد ما. بالإضافة إلى بيانات ملفات التعريف ، يمكن لـ FullJIT إنشاء كود محسّن بدرجة أكبر.

يحتوي JavaScriptCore (يُشار إليه اختصارًا باسم JSC) ، وهو محرك جافا سكريبت من Apple يستخدمه Safari و React Native ، بشكل عام على ثلاثة برامج تجميع مختلفة مختلفة. LLInt هو مترجم منخفض المستوى تم تحسينه إلى برنامج التحويل البرمجي الأساسي ، والذي بدوره تم تحسينه إلى برنامج التحويل البرمجي DFG (تدفق البيانات) ، وتم تحسينه بالفعل إلى برنامج التحويل البرمجي FTL (Faster Than Light).
لماذا بعض المحركات لديها أكثر المجمعين الأمثل من غيرها؟ الأمر كله يتعلق بالتسويات. يمكن للمترجم معالجة الرمز السري بسرعة ، لكن البايت كود وحده ليس فعالًا بشكل خاص. المحول البرمجي الأمثل ، من ناحية أخرى ، يعمل لفترة أطول قليلاً ، لكنه ينتج رمز أكثر كفاءة للماكينة. هذا حل وسط بين الحصول على الكود (المترجم الفوري) بسرعة أو بعض الانتظار وتشغيل الكود مع أقصى أداء (المحول البرمجي الأمثل). تختار بعض المحركات إضافة العديد من برامج التحويل البرمجي المحسّنة ذات الخصائص المختلفة للوقت والكفاءة ، مما يتيح لك توفير أفضل تحكم في هذا الحل الوسط وفهم تكلفة المضاعفات الإضافية للجهاز الداخلي. المفاضلة الأخرى هي استخدام الذاكرة ؛ تحقق من هذه
المقالة للحصول على فهم أفضل.
لقد درسنا للتو الاختلافات الرئيسية بين خطوط أنابيب المترجم المحسن والمترجم لمحركات جافا سكريبت المختلفة. على الرغم من هذه الاختلافات عالية المستوى ، فإن جميع محركات جافا سكريبت لديها نفس البنية: لديهم جميعها محللًا ونوعًا من خطوط أنابيب مترجم / مترجم.
نموذج كائن JavaScriptدعونا نرى ما هو مشترك بين محركات جافا سكريبت وما هي الحيل التي يستخدمونها لتسريع الوصول إلى خصائص كائنات جافا سكريبت؟ اتضح أن جميع المحركات الرئيسية تفعل ذلك بطريقة مماثلة.
تعرّف مواصفات ECMAScript جميع الكائنات على أنها قواميس بمفاتيح سلسلة مطابقة لخصائص
الخاصية .

بالإضافة إلى
[[Value]]
، تحدد المواصفات الخصائص التالية:
[[Writable]]
يحدد ما إذا كان يمكن إعادة تخصيص خاصية ؛[[Enumerable]]
ما إذا كان يتم عرض الخاصية في حلقات for-in ؛[[Configurable]]
يحدد ما إذا كان يمكن حذف خاصية.
يبدو الترقيم
[[ ]]
غريبًا ، لكن هكذا تصف المواصفات الخصائص في JavaScript. لا يزال بإمكانك الحصول على سمات الخصائص هذه لأي كائن وخاصية محددة في JavaScript باستخدام واجهة برمجة تطبيقات
Object.getOwnPropertyDescriptor
:
const object = { foo: 42 }; Object.getOwnPropertyDescriptor(object, 'foo');
حسنًا ، لذلك يقوم JavaScript بتعريف الكائنات. ماذا عن المصفوفات؟
يمكنك أن تتخيل المصفوفات ككائنات خاصة. الفرق الوحيد هو أن المصفوفات لديها معالجة فهرس خاص. هنا ، يعد فهرس الصفيف مصطلحًا خاصًا في مواصفات ECMAScript. يحتوي JavaScript على عدد العناصر في صفيف - ما يصل إلى 2³² - 1. فهرس الصفيف هو أي فهرس متاح من هذا النطاق ، أي قيمة عددية صحيحة من 0 إلى 2³² - 2.
الفرق الآخر هو أن المصفوفات لها خاصية
length
السحر.
const array = ['a', 'b']; array.length;
في هذا المثال ، يبلغ طول الصفيف 2 في وقت الإنشاء. ثم نخصص عنصرًا آخر للفهرس 2 ويزداد الطول تلقائيًا.
يعرّف JavaScript الصفائف وكذلك الكائنات. على سبيل المثال ، يتم تمثيل جميع المفاتيح ، بما في ذلك مؤشرات الصفيف ، صراحة كسلسلة. يتم تخزين العنصر الأول للصفيف تحت المفتاح "0".

خاصية
length
هي مجرد خاصية أخرى تبين أنها غير قابلة للتعداد وغير قابلة للتكوين.
بمجرد إضافة عنصر إلى الصفيف ، يقوم JavaScript تلقائيًا بتحديث سمة الخاصية
[[Value]]
لخاصية
length
.

بشكل عام ، يمكننا القول أن المصفوفات تتصرف بشكل مشابه للأشياء.
تعظيم الاستفادة من الوصول إلى الممتلكاتالآن بعد أن عرفنا كيف يتم تعريف الكائنات في JavaScript ، دعونا نلقي نظرة على كيفية السماح لمحركات JavaScript بالعمل مع الكائنات بكفاءة.
في الحياة اليومية ، يعد الوصول إلى العقارات هو العملية الأكثر شيوعًا. من المهم للغاية للمحرك القيام بذلك بسرعة.
const object = { foo: 'bar', baz: 'qux', };
شكلفي برامج JavaScript ، من الممارسات الشائعة تعيين مفاتيح الخصائص نفسها للعديد من الكائنات. يقولون أن مثل هذه الأشياء لها نفس
الشكل .
const object1 = { x: 1, y: 2 }; const object2 = { x: 3, y: 4 };
أيضًا الميكانيكا الشائعة هي الوصول إلى خاصية الكائنات من نفس الشكل:
function logX(object) { console.log(object.x);
مع العلم بذلك ، يمكن لمحركات JavaScript تحسين الوصول إلى خاصية كائن بناءً على شكله. انظر كيف يعمل
لنفترض أن لدينا كائنًا بخصائص x و y ، يستخدم بنية بيانات القاموس ، التي تحدثنا عنها سابقًا ؛ أنه يحتوي على سلاسل المفاتيح التي تشير إلى سمات كل منها.

إذا قمت بالوصول إلى خاصية ، مثل
object.y,
يبحث محرك JavaScript عن JSObject باستخدام المفتاح
'y'
، ثم يقوم بتحميل خصائص الخاصية التي تطابق هذا الاستعلام وتُرجع أخيرًا
[[Value]]
.
ولكن أين يتم تخزين هذه الخصائص الملكية في الذاكرة؟ يجب علينا تخزينها كجزء من JSObject؟ إذا قمنا بذلك ، فسوف نرى المزيد من الكائنات من هذا النموذج لاحقًا ، وفي هذه الحالة ، يعد هدرًا للمساحة تخزين قاموس كامل يحتوي على أسماء الخصائص والسمات في JSObject نفسها ، نظرًا لتكرار أسماء الممتلكات لجميع الكائنات من نفس النموذج. هذا يسبب الكثير من الازدواجية ويؤدي إلى سوء تخصيص الذاكرة. للتحسين ، تخزن المحركات شكل الكائن بشكل منفصل.

يحتوي هذا
Shape
على جميع أسماء الخصائص والسمات باستثناء
[[Value]]
. بدلاً من ذلك ، يحتوي النموذج على قيم الإزاحة داخل JSObject ، بحيث يعرف مشغل JavaScript مكان البحث عن القيم. يشير كل ملف JSObject بنموذج شائع إلى نسخة محددة من النموذج. الآن يجب على كل JSObject تخزين القيم الفريدة للكائن فقط.

ميزة تصبح واضحة بمجرد أن لدينا الكثير من الأشياء. لا يهم عددهم ، لأنه إذا كان لديهم نموذج واحد ، فإننا نحفظ المعلومات حول النموذج والملكية مرة واحدة فقط.
تستخدم جميع محركات JavaScript النماذج كوسيلة للتحسين ، لكنها لا تسميها مباشرة
shapes
:
- الوثائق الأكاديمية تدعوهم "الفئات المخفية" (مماثلة لفئات جافا سكريبت) ؛
- V8 يدعوهم الخرائط.
- شقرا يدعو لهم أنواع.
- JavaScriptCore يدعو لهم الهياكل.
- SpiderMonkey يطلق عليهم الأشكال.
في هذه المقالة ، ما زلنا نسميها
shapes
.
سلاسل الانتقال والأشجارماذا يحدث إذا كان لديك كائن ذو شكل معين ، لكنك أضفت خاصية جديدة إليه؟ كيف يحدد محرك JavaScript نموذجًا جديدًا؟
const object = {}; object.x = 5; object.y = 6;
تخلق النماذج ما يسمى سلاسل الانتقال في مشغل JavaScript. هنا مثال:

الكائن في البداية ليس له خصائص ، فهو يتوافق مع نموذج فارغ. يضيف التعبير التالي الخاصية
'x'
بالقيمة 5 إلى هذا الكائن ، ثم ينتقل المحرك إلى النموذج الذي يحتوي على الخاصية
'x'
وتضاف القيمة 5 إلى JSObject عند الإزاحة الأولى 0. يضيف السطر التالي الخاصية
'y'
، ثم ينتقل المحرك إلى التالي نموذج يحتوي بالفعل على
'x'
و
'y'
، ويضيف أيضًا القيمة 6 إلى JSObject عند الإزاحة 1.
ملاحظة : التسلسل الذي تضاف به الخصائص يؤثر على النموذج. على سبيل المثال ، سينتج {x: 4، y: 5} شكل مختلف عن {y: 5، x: 4}.
نحن لسنا بحاجة حتى لتخزين جدول الخصائص بأكمله لكل نموذج. بدلاً من ذلك ، يحتاج كل نموذج إلى معرفة خاصية جديدة فقط يحاولون تضمينها فيها. على سبيل المثال ، في هذه الحالة ، لا نحتاج إلى تخزين معلومات حول "x" في النموذج الأخير ، حيث يمكن العثور عليها مسبقًا في السلسلة. لكي ينجح هذا ، يتم دمج النموذج مع النموذج السابق.

إذا قمت بكتابة
ox
في شفرة JavaScript الخاصة بك ، فسوف يبحث JavaScript عن الخاصية
'x'
على طول سلسلة النقل حتى يكتشف نموذجًا يحتوي بالفعل على الخاصية
'x'
.
ولكن ماذا يحدث إذا كان من المستحيل إنشاء سلسلة انتقالية؟ على سبيل المثال ، ماذا يحدث إذا كان لديك كائنين فارغين وقمت بإضافة خصائص مختلفة إليهما؟
const object1 = {}; object1.x = 5; const object2 = {}; object2.y = 6;
في هذه الحالة ، يظهر فرع ، وبدلاً من سلسلة النقل ، نحصل على شجرة انتقال:

ننشئ كائنًا فارغًا ونضيف الخاصية
'x'
. نتيجة لذلك ، لدينا ملف
JSObject
يحتوي على قيمة واحدة
JSObject
: فارغ
JSObject
له خاصية
'x'
واحدة.
المثال الثاني يبدأ بحقيقة أن لدينا كائنًا فارغًا
b
، ولكن بعد ذلك نضيف خاصية أخرى
'y'
. نتيجة لذلك ، هنا نحصل على سلسلتين من الأشكال ، لكن في النهاية نحصل على ثلاث سلاسل.
هل هذا يعني أننا نبدأ دائمًا بنموذج فارغ؟ ليس بالضرورة. تستخدم المحركات بعض التحسينات الحرفية للأشياء ، والتي تحتوي بالفعل على خصائص. لنفترض أننا قمنا بإضافة x ، بدءًا من كائن فارغ حرفي ، أو لدينا كائن حرفي يحتوي بالفعل على
x
:
const object1 = {}; object1.x = 5; const object2 = { x: 6 };
في المثال الأول ، نبدأ بنموذج فارغ وننتقل إلى سلسلة تحتوي أيضًا على
x
، تمامًا كما رأينا سابقًا.
في حالة
object2
من المنطقي إنشاء كائنات لها بالفعل x من البداية ، بدلاً من البدء بكائن فارغ وانتقال.

الحرفي للكائن الذي يحتوي على الخاصية
'x'
يبدأ بنموذج يحتوي على
'x'
من البداية ، ويتم تخطي النموذج الفارغ بشكل فعال. هذا هو (على الأقل) ما يفعله V8 و SpiderMonkey. يعمل التحسين على تقصير سلسلة النقل ويجعلها أكثر ملاءمة لتجميع الكائنات من القيم الحرفية.
يتحدث
منشور مدونة Benedict حول تعدد الأشكال المدهش للتطبيقات على
React حول كيفية تأثير هذه التفاصيل الدقيقة على الأداء.
علاوة على ذلك ، سترى مثالًا لنقاط كائن ثلاثي الأبعاد مع الخصائص
'x'
و
'y'
و
'z'
.
const point = {}; point.x = 4; point.y = 5; point.z = 6;
كما فهمت سابقًا ، فإننا ننشئ كائنًا به ثلاثة أشكال في الذاكرة (بدون حساب النموذج الفارغ). للوصول إلى خاصية
'x'
لهذا الكائن ، على سبيل المثال ، إذا قمت بكتابة
point.x
في البرنامج ، فيجب أن يتبع محرك JavaScript قائمة مرتبطة: بدءًا من النموذج في أسفل الصفحة ، ثم الانتقال تدريجياً إلى النموذج الذي يحتوي على
'x'
في القمة.

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

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