مزايا وعيوب الصفحات الضخمة

مزايا وعيوب الصفحات الضخمة

ترجمة المقال المعد لطلبة المقرر "مسؤول Linux".

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

الجزء 1: التحقق من تمكين الصفحات الضخمة على Linux (النسخة الأصلية). هنا)

المشكلة:
تحتاج إلى التحقق من تمكين HugePages على نظامك.

الحل:
الأمر بسيط جدًا:

cat /sys/kernel/mm/transparent_hugepage/enabled

سوف تحصل على شيء مثل هذا:

always [madvise] never

ستظهر لك قائمة بالخيارات المتاحة (دائما يا مجنون، أبدا)، وسيتم وضع الخيار النشط حاليًا بين قوسين (افتراضيًا com.madvise).

com.madvise يعني أن transparent hugepages تم تمكينه فقط لمناطق الذاكرة التي تطلب بشكل صريح استخدام صفحات ضخمة مادفيز(2).

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

أبدا يعني أن transparent hugepages لن يتم تضمينه حتى عند الطلب باستخدام madvise. لمعرفة المزيد، اتصل توثيق نواة لينكس.

كيفية تغيير القيمة الافتراضية

الخيار 1: التغيير مباشرة sysfs (بعد إعادة التشغيل ستعود المعلمة إلى قيمتها الافتراضية):

echo always >/sys/kernel/mm/transparent_hugepage/enabled
echo madvise >/sys/kernel/mm/transparent_hugepage/enabled
echo never >/sys/kernel/mm/transparent_hugepage/enabled

الخيار 2: قم بتغيير الوضع الافتراضي للنظام عن طريق إعادة ترجمة النواة بتكوين معدل (يوصى بهذا الخيار فقط إذا كنت تستخدم نواة مخصصة):

  • لتعيين الإعداد الافتراضي دائمًا، استخدم:
    CONFIG_TRANSPARENT_HUGEPAGE_ALWAYS=y
    # Comment out CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y
  • لتعيين madvise كإعداد افتراضي، استخدم:
    CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y
    # Comment out CONFIG_TRANSPARENT_HUGEPAGE_ALWAYS=y

الجزء الثاني: مزايا وعيوب الصفحات الضخمة

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

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

وسوف أرفق المزيد من الوصف الفني في الروابط أدناه.

الذاكرة الافتراضية

إذا كنت مبرمجًا بلغة C++، فأنت تعلم أن الكائنات الموجودة في الذاكرة لها عناوين محددة (قيم المؤشر).

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

ولهذا الأسلوب العديد من المزايا، ولكن أهمها:

  • الأداء (لأسباب مختلفة)؛
  • عزل البرنامج، أي أنه لا يمكن لأي برنامج القراءة من ذاكرة برنامج آخر.

ما هي الصفحات؟

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

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

تتميز هذه العملية بالتدفق الشفاف، مما يعني أنها لا تتم بالضرورة قراءتها مباشرةً من محرك الأقراص الثابتة (HDD/SSD). حجم الصفحات العادية هو 4096 بايت. حجم الصفحات الضخمة هو 2 ميجا بايت.

المخزن المؤقت للترجمة (TLB)

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

تحتوي النواة على بنية بيانات (جدول الصفحات) تحتوي على جميع المعلومات حول الصفحات المستخدمة. باستخدام بنية البيانات هذه، يمكنك تعيين عنوان افتراضي إلى عنوان فعلي.

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

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

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

صفحات ضخمة تأتي للإنقاذ

إذن ما الذي يمكننا فعله لتجنب تجاوز TLB؟ (نفترض أن البرنامج لا يزال يحتاج إلى نفس القدر من الذاكرة).

هذا هو المكان الذي تأتي فيه الصفحات الضخمة. بدلاً من أن يتطلب 4096 بايت إدخال TLB واحدًا فقط، يمكن الآن أن يشير إدخال TLB واحد إلى 2 ميغابايت. لنفترض أن TLB يحتوي على 512 إدخالًا، وهنا بدون الصفحات الضخمة يمكننا مطابقتها:

4096 b⋅512=2 MB

فكيف يمكننا المقارنة معهم:

2 MB⋅512=1 GB

هذا هو السبب في أن Hugepages رائع. يمكنهم تحسين الإنتاجية دون بذل الكثير من الجهد. ولكن هناك محاذير كبيرة هنا.

تزوير صفحات ضخمة

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

لنفترض أن لدينا برنامج مثل هذا:

char* mymemory = malloc(2*1024*1024); // Возьмем это за одну Hugepage!
// Заполним mymemory какими-либо данными
// Сделаем много других вещей,
// которые приведут к подмене страницы mymemory
// ...
// Запросим доступ только к первому байту
putchar(mymemory[0]); 

في هذه الحالة، ستحتاج النواة إلى استبدال (قراءة) ما يصل إلى 2 ميجابايت من المعلومات من القرص الصلب/SSD لتتمكن من قراءة بايت واحد فقط. أما بالنسبة للصفحات العادية، فيجب قراءة 4096 بايت فقط من القرص الصلب/SSD.

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

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

التخصيص في الذاكرة

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

char* mymemory = malloc(30);

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

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

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

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

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

الاستخدام الانتقائي للصفحات الضخمة

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

لحسن الحظ يمكنك استخدام madvise()لتمكين الترحيل الضخم فقط لمناطق الذاكرة التي قد يكون من المفيد فيها.

أولاً، تأكد من تشغيل الصفحات الضخمة في وضع madvise() باستخدام تعليمات في بداية المقال.

ثم استخدام madvise()لإخبار النواة بالضبط بمكان استخدام الصفحات الضخمة.

#include <sys/mman.h>
// Аллоцируйте большое количество памяти, которую будете использовать
size_t size = 256*1024*1024;
char* mymemory = malloc(size);
// Просто включите hugepages…
madvise(mymemory, size, MADV_HUGEPAGE);
// … и задайте следующее
madvise(mymemory, size, MADV_HUGEPAGE | MADV_SEQUENTIAL)

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

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

ماذا تقرأ؟

لدي سؤال؟ اكتب في التعليقات!

المصدر: www.habr.com

إضافة تعليق