ننشر اليوم الجزء الثاني من ترجمة المادة المكرسة للآليات الداخلية لـ V8 والتحقيق في مشكلة أداء React.

→
الجزء الأولتقادم وترحيل أشكال الكائنات
ماذا لو احتوى الحقل في البداية على
Smi
، ثم تغير الموقف وتحتاج إلى تخزين قيمة لا
Smi
تمثيل
Smi
مناسبًا لها؟ على سبيل المثال ، كما في المثال التالي ، عندما يتم تمثيل كائنين باستخدام نفس شكل الكائن الذي
x
تخزين
x
البداية كـ
Smi
:
const a = { x: 1 }; const b = { x: 2 };
في بداية المثال ، لدينا كائنان ، نستخدم تمثيلًا له نفس شكل الكائن الذي يُستخدم فيه تنسيق
Smi
لتخزين
x
.
يتم استخدام نفس النموذج لتمثيل الكائناتعندما تتغير خاصية
bx
ويتعين عليك استخدام التنسيق
Double
لتمثيلها ، يخصص V8 مساحة ذاكرة للشكل الجديد للكائن ، حيث يتم تعيين
x
لتمثيل
Double
، والذي يشير إلى نموذج فارغ. V8 أيضا بإنشاء كيان
MutableHeapNumber
، والذي يستخدم لتخزين القيمة 0.2 للخاصية
x
. ثم نقوم بتحديث الكائن (
b
بحيث يشير إلى هذا النموذج الجديد وتغيير الفتحة في الكائن بحيث
MutableHeapNumber
إلى كيان
MutableHeapNumber
تم إنشاؤه مسبقًا عند الإزاحة 0. وأخيراً ، نحتفل
MutableHeapNumber
القديم للكائن على أنه قديم
MutableHeapNumber
عن الشجرة. التحولات. يتم ذلك عن طريق إنشاء انتقال جديد لـ
'x'
من النموذج الفارغ إلى النموذج الذي أنشأناه للتو.
عواقب تعيين خاصية إلى قيمة جديدةفي هذه اللحظة ، لا يمكننا حذف النموذج القديم تمامًا ، حيث لا يزال يتم استخدامه بواسطة الكائن
a
. بالإضافة إلى ذلك ، سيكون تجاوز كل الذاكرة في البحث عن جميع الكائنات التي تشير إلى النموذج القديم مكلفًا للغاية ، وتحديث حالة هذه الكائنات على الفور. بدلا من ذلك ، يستخدم V8 نهج "كسول" هنا. وهي ، جميع العمليات على قراءة أو كتابة خصائص الكائن (
a
نقلها أولاً لاستخدام نموذج جديد. الفكرة من وراء هذا الإجراء هي في النهاية جعل الشكل القديم للكائن بعيد المنال. سيؤدي ذلك إلى جامع البيانات المهملة للتعامل معها.
تحرر الذاكرة خارج النموذج أداة تجميع مجمعي البيانات المهملةالأمور أكثر تعقيدًا في المواقف التي يكون فيها الحقل الذي يغير طريقة العرض ليس الأخير في السلسلة:
const o = { x: 1, y: 2, z: 3, }; oy = 0.1;
في هذه الحالة ، يحتاج V8 إلى العثور على ما يسمى شكل الانقسام. هذا هو النموذج الأخير في السلسلة ، الموجود قبل النموذج الذي تظهر به الخاصية المطابقة. هنا نغير
y
، أي - نحتاج إلى إيجاد النموذج الأخير الذي لم يكن هناك
y
. في مثالنا ، هذا هو الشكل الذي يظهر به
x
.
ابحث عن النموذج الأخير الذي لا توجد فيه قيمة متغيرةهنا ، بدءًا من هذا النموذج ، نقوم بإنشاء سلسلة انتقالية جديدة لـ
y
إنتاج جميع التحولات السابقة. الآن فقط سيتم تمثيل العقار
'y'
على أنه
Double
. الآن نستخدم هذه السلسلة الانتقالية الجديدة لـ
y
، مع وضع علامة عليها كشجرة قديمة قديمة. في الخطوة الأخيرة ، نقوم بترحيل مثيل الكائن
o
إلى نموذج جديد ، باستخدام الكيان
MutableHeapNumber
الآن لتخزين
MutableHeapNumber
y
. باستخدام هذا الأسلوب ، لن يستخدم الكائن الجديد شظايا شجرة الانتقال القديمة ، وبعد اختفاء جميع الإشارات إلى النموذج القديم ، سيختفي الجزء المتقادم من الشجرة أيضًا.
التمدد والنزاهة الانتقال
يتيح لك الأمر
Object.preventExtensions()
منع إضافة خصائص جديدة إلى كائن بشكل كامل. إذا قمت بمعالجة الكائن باستخدام هذا الأمر وحاولت إضافة خاصية جديدة إليه ، فسيتم طرح استثناء. (صحيح ، إذا لم يتم تنفيذ الرمز في وضع صارم ، فلن يتم طرح استثناء ، ومع ذلك ، فإن محاولة إضافة خاصية لن تؤدي ببساطة إلى أي عواقب). هنا مثال:
const object = { x: 1 }; Object.preventExtensions(object); object.y = 2;
يعمل الأسلوب
Object.seal()
على الكائنات بنفس طريقة
Object.preventExtensions()
، لكنه يميز أيضًا جميع الخصائص على أنها غير قابلة للتكوين. هذا يعني أنه لا يمكن حذفها ، ولا يمكن تغيير خصائصها فيما يتعلق بإمكانية إدراجها أو إعدادها أو إعادة كتابتها.
const object = { x: 1 }; Object.seal(object); object.y = 2;
تقوم طريقة
Object.freeze()
بتنفيذ نفس الإجراءات مثل
Object.seal()
، لكن استخدامها ، بالإضافة إلى ذلك ، يؤدي إلى حقيقة أنه لا يمكن تغيير قيم الخصائص الموجودة. يتم تمييزها كخصائص لا يمكن فيها كتابة قيم جديدة.
const object = { x: 1 }; Object.freeze(object); object.y = 2;
النظر في مثال محدد. لدينا عنصرين ، لكل منهما قيمة فريدة
x
. ثم نحظر تمديد الكائن الثاني:
const a = { x: 1 }; const b = { x: 2 }; Object.preventExtensions(b);
تبدأ معالجة هذا الرمز بإجراءات نعرفها بالفعل. وهي ، يتم الانتقال من النموذج الفارغ للكائن إلى النموذج الجديد ، الذي يحتوي على الخاصية
'x'
(تمثل ككيان
Smi
). عندما نحظر توسيع الكائن
b
، يؤدي ذلك إلى انتقال خاص إلى نموذج جديد ، يتم تمييزه على أنه غير قابل للتوسيع. هذا التحول الخاص لا يؤدي إلى ظهور بعض الممتلكات الجديدة. هذا ، في الواقع ، مجرد علامة.
نتيجة معالجة كائن باستخدام الأسلوب Object.preventExtensions ()يرجى ملاحظة أنه لا يمكننا فقط تغيير النموذج الموجود بالقيمة
x
فيه ، حيث إنه مطلوب من قبل كائن آخر ، أي الكائن
a
، والذي لا يزال قابلاً للتوسيع.
الرد على مشكلة الأداء
الآن ، دعونا نجمع كل ما تحدثنا عنه ونستخدم المعرفة التي اكتسبناها لفهم جوهر
مشكلة أداء React الأخيرة. عندما قام فريق React بتوصيف التطبيقات الحقيقية ، لاحظوا تدهورًا غريبًا في أداء V8 الذي عمل على جوهر React. فيما يلي استنساخ مبسط لقسم رمز المشكلة:
const o = { x: 1, y: 2 }; Object.preventExtensions(o); oy = 0.2;
لدينا كائن مع اثنين من الحقول ممثلة ككيانات
Smi
. إننا نمنع زيادة توسيع الكائن ، ثم نقوم بتنفيذ إجراء يؤدي إلى حقيقة أنه يجب تمثيل الحقل الثاني بتنسيق
Double
.
لقد وجدنا بالفعل أن حظر توسيع الكائن يؤدي إلى الموقف التالي تقريبًا.
عواقب حظر توسيع الكائنلتمثيل كلاً من خصائص الكائن ، يتم
Smi
كيانات
Smi
، ويحتاج الأمر إلى الانتقال الأخير من أجل تحديد شكل الكائن على أنه غير قابل للتمديد.
الآن نحن بحاجة إلى تغيير الطريقة التي يتم تمثيل الخاصية
y
بها بواسطة
Double
. هذا يعني أننا نحتاج إلى البدء في البحث عن شكل من أشكال الانفصال. في هذه الحالة ، هذا هو النموذج الذي تظهر به خاصية
x
. ولكن الآن يتم الخلط بين V8. والحقيقة هي أن شكل الفصل كان قابلاً للمد ، وتم وضع علامة على الشكل الحالي على أنه غير قابل للتمديد. V8 لا يعرف كيفية إعادة إنتاج عملية الانتقال في موقف مماثل. نتيجة لذلك ، يرفض المحرك ببساطة محاولة تحديد كل شيء. بدلاً من ذلك ، يقوم ببساطة بإنشاء نموذج منفصل غير متصل بشجرة النموذج الحالية ولا يتم مشاركته مع كائنات أخرى. هذا شيء يشبه شكل يتيم من كائن.
شكل يتيمةمن السهل تخمين أن هذا ، إذا حدث هذا مع العديد من الكائنات ، أمر سيء للغاية. والحقيقة هي أن هذا يجعل النظام الكامل للكائن V8 عديمة الفائدة.
عندما حدثت مشكلة React الأخيرة ، حدث ما يلي. يحتوي كل كائن من فئة
FiberNode
حقول كانت تهدف إلى تخزين الطوابع الزمنية عند تمكين
FiberNode
.
class FiberNode { constructor() { this.actualStartTime = 0; Object.preventExtensions(this); } } const node1 = new FiberNode(); const node2 = new FiberNode();
تمت تهيئة هذه الحقول (على سبيل المثال ،
actualStartTime
) إلى 0 أو -1. هذا أدى إلى حقيقة أن كيانات
Smi
كانت تستخدم لتمثيل معانيها
Smi
. لكن فيما بعد ، قاموا بحفظ طوابع الوقت الفعلي بتنسيق أرقام الفاصلة العائمة التي يتم إرجاعها بواسطة الأسلوب
performance.now (). هذا أدى إلى حقيقة أنه لم يعد من الممكن تمثيل هذه القيم في شكل
Smi
. لتمثيل هذه الحقول ، أصبحت الكيانات
Double
مطلوبة الآن. علاوة على ذلك ، منع
FiberNode
أيضًا توسيع مثيلات فئة
FiberNode
.
في البداية ، يمكن تقديم مثالنا المبسط في النموذج التالي.
الحالة الأولية للنظامهناك حالتان من الفصل تشتركان في نفس شجرة التحولات في شكل الكائنات. بالمعنى الدقيق للكلمة ، هذا هو ما تم تصميم نظام أشكال الكائنات في V8 له. ولكن بعد ذلك ، عندما يتم تخزين طوابع الوقت الفعلي في الكائن ، لا يستطيع V8 فهم كيفية العثور على شكل الفصل.
V8 مرتبكيعيّن V8 نموذج المعزول الجديد إلى
node1
. يحدث نفس الشيء لاحقًا مع كائن
node2
. نتيجة لذلك ، لدينا الآن نموذجان "يتيمان" ، يتم استخدام كل منهما بواسطة كائن واحد فقط. في العديد من تطبيقات React الحقيقية ، يكون عدد هذه الكائنات أكثر من اثنين. يمكن أن تكون هذه عشرات أو حتى آلاف الكائنات من فئة
FiberNode
. من السهل أن نفهم أن هذا الموقف لا يؤثر بشكل جيد على أداء V8.
لحسن الحظ ، قمنا
بإصلاح هذه المشكلة في
الإصدار V8 v7.4 ، ونحن
نستكشف إمكانية جعل عملية تغيير تمثيل حقول الكائنات أقل كثافة في استخدام الموارد. سيسمح لنا ذلك بحل مشاكل الأداء المتبقية التي تنشأ في مثل هذه الحالات. V8 ، بفضل الإصلاح ، يتصرف الآن بشكل صحيح في حالة المشكلة الموصوفة أعلاه.
الحالة الأولية للنظامهنا هو كيف يبدو.
FiberNode
الفئة
FiberNode
إلى نموذج غير قابل للتوسيع. في هذه الحالة ، يتم تمثيل
'actualStartTime'
. عند تنفيذ العملية الأولى لتعيين قيمة إلى
node1.actualStartTime
، يتم إنشاء سلسلة انتقال جديدة ، ويتم وضع علامة على السلسلة السابقة على أنها قديمة.
نتائج تعيين قيمة جديدة لخاصية Node1.actualStartTimeيرجى ملاحظة أن الانتقال إلى النموذج غير القابل للتوسيع الآن مستنسخ بشكل صحيح في السلسلة الجديدة. هذا هو ما يحصل عليه النظام بعد تغيير قيمة
node2.actualStartTime
.
نتائج تعيين قيمة جديدة للخاصية node2.actualStartTimeبعد تعيين القيمة الجديدة للخاصية
node2.actualStartTime
، يشير كلا الكائنين إلى النموذج الجديد ، ويمكن إتلاف الجزء المتقادم من شجرة النقل بواسطة أداة تجميع مجمعي البيانات المهملة.
يرجى ملاحظة أن عمليات وضع علامات على أشكال الكائنات على أنها قديمة وترحيلها قد تبدو شيئًا معقدًا. في الواقع - على ما هو عليه. نشك في أن هذا على مواقع الويب الحقيقية يضر (من حيث الأداء واستخدام الذاكرة والتعقيد) أكثر من نفعه. خاصةً - بعد ، في حالة
ضغط المؤشر ، لم يعد بإمكاننا استخدام هذا الأسلوب لتخزين الحقول
Double
في شكل قيم مضمن في الكائنات. نتيجة لذلك ، نأمل في
التخلي تمامًا عن آلية تقادم أشكال الكائنات V8 وجعل هذه الآلية نفسها قديمة.
تجدر الإشارة إلى أن فريق React قام
بحل هذه المشكلة من تلقاء نفسه ، مع التأكد من أن الحقول الموجودة في كائنات فئة
FiberNodes
تمثيلها في البداية بقيم مزدوجة:
class FiberNode { constructor() {
هنا ، بدلاً من
Number.NaN
، يمكن استخدام أي قيمة
Number.NaN
العائمة التي لا تناسب نطاق
Smi
. من بين هذه القيم 0.000001 و
Number.MIN_VALUE
و -0 و
Infinity
.
تجدر الإشارة إلى أن المشكلة الموضحة في React كانت خاصة بـ V8 ، وأنه عند إنشاء بعض التعليمات البرمجية ، لا يحتاج المطورون إلى السعي لتحسينها استنادًا إلى إصدار محدد من محرك JavaScript معين. ومع ذلك ، من المفيد أن تكون قادرًا على إصلاح شيء ما عن طريق تحسين التعليمة البرمجية في حالة أن أسباب بعض الأخطاء متجذرة في ميزات المحرك.
تجدر الإشارة إلى أن هناك الكثير من الأشياء الرائعة في أحشاء محركات JS. يمكن لمطور JS مساعدة كل هذه الآليات ، إذا أمكن ذلك دون تعيين قيم متغيرات نفس الأنواع المختلفة. على سبيل المثال ، يجب عدم تهيئة الحقول الرقمية لتكون
null
، حيث إن هذا سيؤدي إلى إلغاء جميع مزايا مراقبة تمثيل الحقل وتحسين إمكانية قراءة الكود:
وبعبارة أخرى - كتابة التعليمات البرمجية القابلة للقراءة ، وسيأتي الأداء من تلقاء نفسه!
النتائج
في هذه المقالة ، درسنا المشكلات الهامة التالية:
- يميز JavaScript بين القيم "البدائية" و "الكائن" ، ولا يمكن الوثوق بنتائج
typeof
. - حتى القيم التي لها نفس نوع JavaScript يمكن تمثيلها بطرق مختلفة في أحشاء المحرك.
- تحاول V8 إيجاد أفضل طريقة لتمثيل كل خاصية للكائن المستخدم في برامج JS.
- في بعض المواقف ، تقوم V8 بعمليات وضع علامات على أشكال الكائنات على أنها قديمة وتجري عملية ترحيل النماذج. بما في ذلك - ينفذ التحولات المرتبطة بحظر توسيع الكائنات.
بناءً على ما تقدم ، يمكننا تقديم بعض النصائح العملية لبرمجة JavaScript والتي يمكن أن تساعد في تحسين أداء الكود:
- تهيئة الأشياء الخاصة بك دائما بنفس الطريقة. هذا يساهم في العمل الفعال مع أشكال الكائنات.
- حدد المسؤول القيم الأولية لحقول الكائنات. سيساعد ذلك محركات JavaScript في اختيار كيفية تمثيل هذه القيم داخليًا.
أعزائي القراء! هل قمت بتحسين الشفرة على أساس الميزات الداخلية لبعض محركات جافا سكريبت؟
