جمع القمامة في V8: كيف يعمل Orinoco GC الجديد

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



على مدى السنوات القليلة الماضية ، تغير نهج جمع القمامة في V8 كثيرًا. كجزء من مشروع Orinoco ، انتقل من منهج ثابت للعالم إلى مقاربات متوازية وتنافسية مع تراجع تدريجي.

ملاحظة: إذا كنت تفضل مشاهدة التقرير من قراءة المقالة ، فيمكنك القيام بذلك هنا . إذا لم يكن كذلك ، ثم قرأ على.

أي جامع للقمامة لديه مجموعة من المهام التي يجب القيام بها بشكل دوري:

  1. العثور على الكائنات الحية / القتلى في الذاكرة.
  2. إعادة استخدام الذاكرة التي تحتلها الكائنات الميتة.
  3. ذاكرة مدمجة / إلغاء تجزئة (اختياري).

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

GC الرئيسية (علامة كاملة مدمجة)


يجمع GC الرئيسي القمامة من الكومة بالكامل.

يتم تنظيف القمامة على ثلاث مراحل: وضع العلامات والتخلص والضغط

وسم


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

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

انتعاش


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

ختم


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

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

جهاز توليد الذاكرة


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

تم تقسيم مجموعة من V8 إلى أجيال. تنتقل الكائنات من أصغر إلى أكبر إذا نجت من جمع القمامة

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

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

مساعد GC (زبال)


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

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

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

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

يقوم الزبال بنقل الكائنات الباقية إلى صفحة ذاكرة جديدة

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

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

يقوم الزبال بنقل الكائنات "الوسيطة" إلى الذاكرة القديمة ، والكائنات من "المهد" - إلى صفحة جديدة

وبالتالي ، تتكون مجموعة البيانات المهملة في الذاكرة الناشئة من ثلاث خطوات: وضع علامات على الكائنات ، ونسخها ، وتحديث المؤشرات.

أورينوكو


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

Orinoco GC V8 Logo

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

تواز


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

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

مؤشرات الترابط الرئيسية والمساعدة تعمل على نفس المهمة في نفس الوقت

تدريجي


تزايدي هو عندما الخيط الرئيسي لا كمية صغيرة من العمل بشكل متقطع. بدلاً من تجميع البيانات المهملة كاملة ، تتم مهام صغيرة لجمع جزئي.

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

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

مساحات صغيرة من GC العمل في الموضوع الرئيسي

القدرة التنافسية


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

علاوة على ذلك ، توجد أيضًا سباقات للقراءة / الكتابة ، حيث أن التدفقات المساعدة والرئيسية تقرأ نفس الأشياء أو تعدّلها في وقت واحد.

يتم التجميع بالكامل في الخلفية ، حيث يمكن للخيط الرئيسي في هذا الوقت تنفيذ JavaScript

حالة GC في V8


الكسح


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

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

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

يقوم التجميع المتوازي بتوزيع العمل بين عدة مؤشرات ترابط مساعدة وخيط رئيسي

جي سي الأساسية


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

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


يستخدم GC الأساسي وضع العلامات التنافسية والتخلص والضغط الموازي وتحديث المؤشرات

في نهاية التصنيف التنافسي ، ينفذ الخيط الرئيسي خطوة سريعة لإنهاء وضع العلامات. أثناء هذا ، يتم إيقاف تنفيذ JavaScript في سلسلة الرسائل الرئيسية.

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

أثناء هذا التوقف المؤقت ، يتم تشغيل المهام الكاسحة التي تتنافس مع مهام ضغط الذاكرة والموضوع الرئيسي ويمكن أن تستمر حتى عند تنفيذ JavaScript في سلسلة الرسائل الرئيسية.

الخمول الوقت GC


لا يستطيع مطورو JavaScript الوصول إلى GC - فهو جزء من بيئة التنفيذ. وعلى الرغم من أن كود JS لا يمكنه استدعاء GC مباشرة ، فإن V8 يوفر هذا الوصول إلى البيئة التي تضم المحرك.

يمكن لـ GC إرسال المهام (المهام الخاملة) التي يمكن القيام بها "في وقت فراغك" والتي هي جزء من العمل الذي يجب القيام به على أي حال. قد يكون لدى بيئة مثل Chrome ، حيث يتم تضمين المحرك ، فكرة عما يمكن اعتباره وقت فراغ. على سبيل المثال ، في Chrome ، بمعدل إطار يبلغ 60 إطارًا في الثانية ، لدى المتصفح 16.6 مللي ثانية تقريبًا لتقديم إطار للرسوم المتحركة.

إذا تم الانتهاء من أعمال الرسوم المتحركة في وقت مبكر ، في وقت فراغك ، قبل الإطار التالي ، فيمكن لـ Chrome أداء بعض المهام المستلمة من GC.

يستخدم GC وقت الفراغ الرئيسي للتنظيف المسبق

يمكن الاطلاع على التفاصيل في منشورنا على Idle-time GC .

النتائج


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

تم تحسين كل ما يتعلق بإيقاف الدفق الرئيسي ووقت الاستجابة وتحميل الصفحة بشكل كبير ، مما يتيح جعل الرسوم المتحركة والتمرير وتفاعل المستخدم على الصفحة أكثر سلاسة. يسمح المجمع الجامع بالتقليل من إجمالي وقت معالجة الذاكرة الناشئة بنسبة 20-50٪ ، اعتمادًا على الحمل.

يقلل Idle-time GC من حجم الكومة المستخدمة لـ Gmail بنسبة 45٪. يمكن أن تؤدي الملصقات والتخلص التنافسيين (الكنس) إلى تقليل مدة توقف GC في ألعاب WebGL الثقيلة بنسبة تصل إلى 50٪.

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

علاوة على ذلك ، تم تجهيز Blink (عارض في Chrome) أيضًا بجامع (Oilpan) ، ونعمل على تحسين التفاعل بين GCs ، وكذلك لاستخدام تقنيات Orinoco في Oilpan.

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

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


All Articles