تحسين العمل باستخدام النماذج الأولية في محركات JavaScript

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



مستويات تحسين الشفرة والمفاضلات


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

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

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

هذا هو نموذج إعداد التعليمات البرمجية للتنفيذ الذي يستخدم في V8. يسمى مترجم V8 Ignition ، وهو أسرع المترجمين الحاليين (من حيث تنفيذ رمز المصدر المصدر). يسمى مترجم V8 الأمثل TurboFan ، وهو المسؤول عن إنشاء رمز آلة محسن للغاية.
مترجم الإشعال و TurboFan الأمثل المترجم

المفاضلة بين التأخير في بدء البرنامج وسرعة التنفيذ هو السبب في أن بعض محركات JS لديها مستويات إضافية من التحسين. على سبيل المثال ، في SpiderMonkey ، بين المترجم والمترجم الأمثل IonMonkey ، هناك مستوى متوسط ​​يمثله المترجم الأساسي (يطلق عليه "مترجم الخط الأساسي" في وثائق موزيلا ، ولكن "الخط الأساسي" ليس اسمًا مناسبًا).
مستويات تحسين رمز SpiderMonkey

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

دعنا نلقي نظرة على مثال محدد ونلقي نظرة على كيفية تعامل خطوط الأنابيب الخاصة بالمحركات المختلفة مع الكود. في المثال المقدم هنا ، هناك حلقة "ساخنة" تحتوي على كود يتكرر مرات عديدة.

let result = 0; for (let i = 0; i < 4242424242; ++i) {    result += i; } console.log(result); 

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

أثناء التحسين ، يستمر V8 في تنفيذ البايت كود في Ignition. عند اكتمال المُحسِّن ، لدينا رمز جهاز قابل للتنفيذ يمكن استخدامه في المستقبل.

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

إذا كانت الشفرة الأساسية تعمل لفترة كافية ، فسيطلق SpiderMonkey في النهاية الواجهة الأمامية والمحسن IonMonkey ، وهو ما يشبه إلى حد كبير ما يحدث في V8. يستمر تشغيل الكود الأساسي كجزء من عملية تحسين الكود التي يقوم بها IonMonkey. ونتيجة لذلك ، عند اكتمال التحسين ، يتم تنفيذ الكود المُحسّن بدلاً من الكود الأساسي.

تشبه بنية محرك شقرا إلى حد كبير بنية SpiderMonkey ، لكن شقرا تسعى إلى مستوى أعلى من التزامن لتجنب حظر الخيط الرئيسي. بدلاً من حل أي مهام ترجمة في الخيط الرئيسي ، تقوم Chakra بنسخ وإرسال بيانات البايت كود والتوصيف التي من المحتمل أن يحتاجها المترجم في عملية تجميع منفصلة.
تحسين الشفرة الساخنة في شقرا

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

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

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

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

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

في ما يلي الرمز الثانوي لوظيفة add التي تم إنشاؤها بواسطة مترجم الإشعال في V8:

 StackCheck Ldar a1 Add a0, [0] Return 

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

عندما تكون قطعة من هذا الرمز "ساخنة" ، يتم استخدام TurboFan ، مما يؤدي إلى إنشاء رمز الجهاز المحسن للغاية التالي:

 leaq rcx,[rip+0x0] movq rcx,[rcx-0x37] testb [rcx+0xf],0x1 jnz CompileLazyDeoptimizedCode push rbp movq rbp,rsp push rsi push rdi cmpq rsp,[r13+0xe88] jna StackOverflow movq rax,[rbp+0x18] test al,0x1 jnz Deoptimize movq rbx,[rbp+0x10] testb rbx,0x1 jnz Deoptimize movq rdx,rbx shrq rdx, 32 movq rcx,rax shrq rcx, 32 addl rdx,rcx jo Deoptimize shlq rdx, 32 movq rax,rdx movq rsp,rbp pop rbp ret 0x18 

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

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

تحسين الوصول إلى خصائص النموذج الأولي للكائن


تعمل محركات JavaScript على تحسين الوصول إلى خصائص الكائن من خلال استخدام ما يسمى نماذج الكائنات (الشكل) وذاكرة التخزين المؤقت المضمنة (ذاكرة التخزين المؤقت المضمنة ، IC). يمكن قراءة تفاصيل هذا في هذه المادة ، ولكن لوضعه باختصار ، يمكننا القول أن المحرك يخزن شكل الكائن بشكل منفصل عن قيم الكائن.
كائنات لها نفس الشكل

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

الفئات والنماذج


الآن بعد أن عرفنا كيفية تسريع الوصول إلى خصائص الكائن في JavaScript ، ألق نظرة على أحد ابتكارات JavaScript الحديثة - الفئات. إليك ما يبدو عليه إعلان الفصل:

 class Bar {   constructor(x) {       this.x = x;   }   getX() {       return this.x;   } } 

على الرغم من أنه قد يبدو وكأنه مظهر في JS لمفهوم جديد تمامًا ، إلا أن الفئات هي في الواقع مجرد سكر نحوي لنظام النموذج الأولي لبناء الكائنات ، والتي كانت موجودة دائمًا في JavaScript:

 function Bar(x) {   this.x = x; } Bar.prototype.getX = function getX() {   return this.x; }; 

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

دعونا نلقي نظرة على ما يحدث ، إذا جاز التعبير ، خلف الكواليس عندما نقوم بإنشاء مثيل جديد لكائن Bar ، وتعيينه إلى foo المستمر.

 const foo = new Bar(true); 

بعد تنفيذ مثل هذا الرمز ، سيكون لمثيل الكائن الذي تم إنشاؤه هنا نموذج يحتوي على خاصية واحدة x . النموذج الأولي لكائن foo هو Bar.prototype ، الذي ينتمي إلى Bar الصنف.
الكائن ونموذجه الأولي

يحتوي Bar.prototype على نموذج خاص به يحتوي على خاصية getX مفردة قيمتها هي دالة تقوم ، عند استدعاؤها ، بإرجاع قيمة this.x النموذج الأولي Bar.prototype هو Object.prototype ، وهو جزء من اللغة. Object.prototype هو العنصر الجذر لشجرة النموذج الأولي ، لذا فإن النموذج الأولي null .

الآن دعونا نرى ما يحدث إذا قمت بإنشاء كائن آخر من النوع Bar .
عدة أشياء من نفس النوع

كما ترون ، qux كل من كائن foo وكائن qux ، وهما مثيلات لفئة Bar ، كما قلنا بالفعل ، نفس شكل الكائن. كلاهما يستخدم نفس النموذج الأولي - كائن Bar.prototype .

الوصول إلى خصائص النموذج الأولي


إذن نحن الآن نعرف ما يحدث عندما نعلن عن فئة جديدة وننشئها. وماذا عن الدعوة إلى طريقة الكائن؟ خذ بعين الاعتبار مقتطف الرمز التالي:

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); //        ^^^^^^^^^^ 

يمكن فهم استدعاء الأسلوب على أنه عملية تتكون من خطوتين:

 const x = foo.getX(); //         : const $getX = foo.getX; const x = $getX.call(foo); 

في الخطوة الأولى ، يتم تحميل الطريقة ، وهي مجرد خاصية للنموذج الأولي (قيمتها هي الوظيفة). في الخطوة الثانية ، يتم استدعاء دالة مع this المجموعة. خذ بعين الاعتبار الخطوة الأولى في تحميل طريقة getX من كائن foo :
تحميل طريقة getX من كائن foo

يحلل المحرك كائن foo ويكتشف أنه لا توجد خاصية getX في شكل كائن foo . هذا يعني أن المحرك يحتاج إلى النظر في سلسلة النموذج للكائن من أجل العثور على هذه الطريقة. يصل المحرك إلى النموذج الأولي Bar.prototype وينظر في شكل كائن هذا النموذج الأولي. هناك ، يجد الخاصية المطلوبة في الإزاحة 0. بعد ذلك ، يتم الوصول إلى القيمة المخزنة في هذا الإزاحة في Bar.prototype ، يتم اكتشاف JSFunction getX هناك - وهذا بالضبط ما نبحث عنه. هذا يكمل البحث عن الطريقة.

مرونة JavaScript تجعل من الممكن تغيير سلاسل النموذج الأولي. على سبيل المثال ، مثل هذا:

 const foo = new Bar(true); foo.getX(); // true Object.setPrototypeOf(foo, null); foo.getX(); // Uncaught TypeError: foo.getX is not a function 

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

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

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); //        ^^^^^^^^^^ 

في وقت سابق ، تحدثنا عن كيفية تحسين المحركات لتحميل الخصائص العادية والمخصصة للكائنات من خلال استخدام نماذج الكائنات وذاكرة التخزين المؤقت المضمنة. كيفية تحسين خصائص النموذج الأولي المتكررة لتحميل الكائنات ذات الشكل نفسه؟ أعلاه ، رأينا كيف يتم تحميل الخصائص.
تحميل طريقة getX من كائن foo

من أجل تسريع الوصول إلى الطريقة من خلال المكالمات المتكررة إليها ، في حالتنا ، تحتاج إلى معرفة ما يلي:

  1. لا يحتوي شكل كائن foo على طريقة getX ولا يتغير. هذا يعني أن كائن foo لا يتم تعديله عن طريق إضافة خصائص إليه أو حذفها أو تغيير سمات الخصائص.
  2. لا يزال النموذج الأولي foo هو النموذج Bar.prototype الأصلي. هذا يعني أن النموذج الأولي foo لا يتغير باستخدام طريقة Object.setPrototypeOf() أو عن طريق تعيين نموذج أولي جديد لخاصية _proto_ الخاصة.
  3. يحتوي النموذج Bar.prototype على getX ولا يتغير. بمعنى ، لا Bar.prototype تغيير Bar.prototype عن طريق حذف الخصائص أو إضافتها أو تغيير سماتها.

في الحالة العامة ، هذا يعني أننا بحاجة إلى إجراء فحص واحد للكائن نفسه ، وتحققين من كل نموذج أولي يصل إلى النموذج الأولي الذي يخزن الخاصية التي نبحث عنها. أي أنك تحتاج إلى إجراء عمليات فحص 1 + 2N (حيث N هو عدد النماذج الأولية المختبرة) ، والتي في هذه الحالة لا تبدو سيئة للغاية ، لأن سلسلة النموذج الأولي قصيرة جدًا. ومع ذلك ، غالبًا ما يتعين على المحركات العمل مع سلاسل نموذجية أطول بكثير. هذا ، على سبيل المثال ، نموذجي لعناصر DOM العادية. هنا مثال:

 const anchor = document.createElement('a'); // HTMLAnchorElement const title = anchor.getAttribute('title'); 

هنا لدينا HTMLAnchorElement getAttribute() أسلوب getAttribute() . تتضمن سلسلة النموذج الأولي لهذا العنصر البسيط الذي يمثل رابط HTML 6 نماذج أولية! معظم طرق DOM المثيرة ليست في النموذج الأولي الخاص بها HTMLAnchorElement . وهي في نماذج أولية تقع أسفل السلسلة.
سلسلة النموذج الأولي

يمكن العثور على طريقة getAttribute() في Element.prototype . هذا يعني أنه في كل مرة ، عندما يتم anchor.getAttribute() الأسلوب anchor.getAttribute() ، يضطر المحرك إلى تنفيذ الإجراءات التالية:

  1. للتحقق من كائن anchor نفسه لـ getAttribute .
  2. التحقق من أن النموذج الأولي للكائن هو HTMLAnchorElement.prototype .
  3. معرفة أن HTMLAnchorElement.prototype ليس لديه طريقة getAttribute .
  4. التحقق من أن النموذج الأولي التالي هو HTMLElement.prototype .
  5. معرفة أنه لا توجد طريقة ضرورية هنا.
  6. أخيرًا ، اكتشف أن النموذج الأولي التالي هو Element.prototype .
  7. معرفة أن هناك طريقة getAttribute .

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

إذا عدنا إلى أحد الأمثلة السابقة ، يمكننا أن نتذكر أنه عندما نسمي طريقة getX لكائن getX ، فإننا نجري 3 عمليات تحقق:

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 

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

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

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

خاصية ValidityCell


يشير V8 إلى أشكال النماذج على وجه التحديد للغرض أعلاه. يحتوي كل نموذج أولي على شكل فريد لا تتم مشاركته مع كائنات أخرى (على وجه الخصوص ، مع نماذج أولية أخرى) ، ولكل من نماذج الكائن النموذجي خاصية ValidityCell المرتبطة بها.
خاصية ValidityCell

تم إعلان هذه الخاصية غير صالحة عند تغيير النموذج الأولي المرتبط بالنموذج ، أو أي نموذج أولي مغطي. النظر في هذه الآلية بمزيد من التفصيل.

من أجل تسريع العمليات المتتالية لخصائص التحميل من النماذج الأولية ، يستخدم V8 ذاكرة تخزين مؤقت مضمنة تحتوي على أربعة حقول: ValidityCell ، Prototype ، Shape ، Offset .
حقول ذاكرة التخزين المؤقت المضمنة

أثناء "تدفئة" ذاكرة التخزين المؤقت المضمنة في المرة الأولى التي يتم فيها تشغيل الكود ، يتذكر V8 الإزاحة التي تم العثور فيها على الخاصية في النموذج الأولي ، والنموذج الأولي الذي تم العثور على الخاصية فيه (في هذا المثال ، Bar.prototype ) ، شكل الكائن ( foo في هذه الحالة) ، بالإضافة إلى ذلك ، رابط إلى معلمة ValidityCell الحالية للنموذج الأولي المباشر ، وهو ارتباط يكون في شكل كائن (في هذه الحالة ، يكون أيضًا Bar.prototype ).

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

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

إذا عدنا إلى المثال باستخدام عنصر DOM ، فإن هذا يعني أن أي تغيير ، على سبيل المثال ، في النموذج الأولي لـ Object.prototype ، لن يؤدي فقط إلى إبطال ذاكرة التخزين المؤقت المضمنة لـ Object.prototype نفسه ، ولكن أيضًا لأي نماذج أولية تقع تحته في سلسلة النموذج الأولي بما في ذلك EventTarget.prototype و Node.prototype و Element.prototype وما إلى ذلك ، وصولاً إلى HTMLAnchorElement.prototype .
آثار تغيير Object.prototype

في الواقع ، تعديل Object.prototype أثناء تنفيذ التعليمات البرمجية يعني إحداث ضرر خطير في الأداء. لا تفعل هذا.

ندرس ما سبق مع مثال. افترض أن لدينا فئة Bar ، ووظيفة loadX ، التي تستدعي طريقة الكائنات التي تم إنشاؤها من فئة Bar . نسمي وظيفة loadX عدة مرات ، loadX مثيلات من نفس الفئة.

 function loadX(bar) {   return bar.getX(); // IC  'getX'   `Bar`. } loadX(new Bar(true)); loadX(new Bar(false)); // IC  `loadX`    `ValidityCell`  // `Bar.prototype`. Object.prototype.newMethod = y => y; // `ValidityCell`  IC `loadX`   //    `Object.prototype`  . 

تشير ذاكرة التخزين المؤقت loadX في loadX الآن إلى ValidityCell لـ Bar.prototype . , , Object.prototype — JavaScript, ValidityCell , - , .

Object.prototype — , - , . , :

 Object.prototype.foo = function() { /* … */ }; //    : someObject.foo(); //     . delete Object.prototype.foo; 

Object.prototype , - , . , . - , . , « », , .

, , . . Object.prototype , , - .

, — , JS- - , . . , , . , , , .

الملخص


, JS- , , , -, ValidityCell , . JavaScript, , ( , , , ).

أعزائي القراء! , - , JS, ?

Source: https://habr.com/ru/post/ar422321/


All Articles