سؤال

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

هل هناك قاعدة عامة للاستخدام mmap() مقابل القراءة في كتل عبر C++ fstream مكتبة؟ما أود القيام به هو قراءة كتل كبيرة من القرص إلى المخزن المؤقت، ومعالجة السجلات الكاملة من المخزن المؤقت، ثم قراءة المزيد.

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

كيف يمكنني الاختيار بين هذين الخيارين دون كتابة التنفيذ الكامل أولاً؟أي قواعد أساسية (على سبيل المثال، mmap() هل هو أسرع مرتين) أم اختبارات بسيطة؟

هل كانت مفيدة؟

المحلول

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

  • دعوة ل mmap لديها المزيد من النفقات العامة من read (تماما مثل epoll لديها المزيد من النفقات العامة من poll, ، والتي لديها الحمل أكثر من read).يعد تغيير تعيينات الذاكرة الافتراضية عملية مكلفة للغاية على بعض المعالجات لنفس الأسباب التي تجعل التبديل بين العمليات المختلفة مكلفًا.
  • يمكن لنظام الإدخال والإخراج استخدام ذاكرة التخزين المؤقت على القرص بالفعل، لذلك إذا قرأت ملفًا، فسوف تصل إلى ذاكرة التخزين المؤقت أو تفوتها بغض النظر عن الطريقة التي تستخدمها.

لكن،

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

تذكرني مناقشة mmap/read بمناقشتين أخريين للأداء:

  • لقد صُدم بعض مبرمجي Java عندما اكتشفوا أن الإدخال/الإخراج غير المحظور غالبًا ما يكون أبطأ من حظر الإدخال/الإخراج، وهو أمر منطقي تمامًا إذا كنت تعلم أن عدم حظر الإدخال/الإخراج يتطلب إجراء المزيد من مكالمات النظام.

  • لقد صدم بعض مبرمجي الشبكات الآخرين عندما علموا بذلك epoll غالبا ما يكون أبطأ من poll, ، وهو أمر منطقي تمامًا إذا كنت تعرف تلك الإدارة epoll يتطلب إجراء المزيد من مكالمات النظام.

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

(آسف على طرح هذا السؤال، ولكنني كنت أبحث عن إجابة وظل هذا السؤال يظهر في أعلى نتائج Google.)

نصائح أخرى

ستكون تكلفة الأداء الرئيسية هي إدخال/إخراج القرص.من المؤكد أن "mmap()" أسرع من istream، ولكن قد لا يكون الفرق ملحوظًا لأن إدخال/إخراج القرص سيهيمن على أوقات التشغيل.

لقد جربت جزء التعليمات البرمجية الخاص بـ Ben Collins (انظر أعلاه/أدناه) لاختبار تأكيده على أن "mmap() هو طريق أسرع" ولم أجد أي فرق ملموس.انظر تعليقاتي على إجابته.

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

في حالتك، أعتقد أن mmap() وistream والمكالمات ذات المستوى المنخفض open()/read() ستكون جميعها متماثلة تقريبًا.أوصي بـ mmap() في هذه الحالات:

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

(راجع للشغل - أنا أحب mmap ()/MapViewOfFile ()).

mmap هو طريق أسرع.يمكنك كتابة معيار بسيط لإثبات ذلك لنفسك:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

عكس:

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

من الواضح أنني أترك التفاصيل (مثل كيفية تحديد الوقت الذي تصل فيه إلى نهاية الملف في حالة أن ملفك ليس من مضاعفاته) page_size, ، على سبيل المثال)، ولكن لا ينبغي أن يكون الأمر أكثر تعقيدًا من هذا.

إذا استطعت، فقد تحاول تقسيم بياناتك إلى ملفات متعددة يمكن أن تكون mmap()-ed كاملة بدلاً من جزئية (أبسط بكثير).

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

تحديث:يجب أن أضيف أيضًا التحذير بأن هذا المعيار سيبدو مختلفًا تمامًا في Windows لأن Microsoft قامت بتطبيق ذاكرة تخزين مؤقت أنيقة للملفات تقوم بمعظم ما ستفعله باستخدام mmap في المقام الأول.على سبيل المثال، بالنسبة للملفات التي يتم الوصول إليها بشكل متكرر، يمكنك فقط إجراء std::ifstream.read() وسيكون ذلك بنفس سرعة mmap، لأن ذاكرة التخزين المؤقت للملف كانت قد قامت بالفعل بتعيين الذاكرة لك، وهي شفافة.

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

تحرير لتنظيف قائمة الإجابات:@جي بي ال:

النافذة المنزلق mmap تبدو مثيرة للاهتمام.هل يمكنك أن تقول المزيد عنها؟

بالتأكيد - كنت أكتب مكتبة C++ لـ Git (libgit++، إذا صح التعبير)، وواجهت مشكلة مشابهة لهذه:كنت بحاجة إلى أن أكون قادرًا على فتح ملفات كبيرة (كبيرة جدًا) وألا يكون الأداء ككل (كما هو الحال مع std::fstream).

Boost::Iostreams لديه بالفعل مصدر Mapped_file، ولكن المشكلة أنه كان كذلك mmapتنفيذ الأمر ping للملفات بأكملها، مما يحدك من 2^(حجم الكلمات).على أجهزة 32 بت، 4 غيغابايت ليست كبيرة بما يكفي.ليس من غير المعقول أن نتوقع أن يكون .pack الملفات الموجودة في Git والتي أصبحت أكبر بكثير من ذلك، لذلك كنت بحاجة إلى قراءة الملف في أجزاء دون اللجوء إلى إدخال/إخراج الملف العادي.تحت أغطية Boost::Iostreams, لقد قمت بتنفيذ مصدر، وهو وجهة نظر أخرى إلى حد ما للتفاعل بين std::streambuf و std::istream.يمكنك أيضًا تجربة نهج مماثل عن طريق الميراث فقط std::filebuf الى mapped_filebuf وكذلك الميراث std::fstream داخل a mapped_fstream.إنه التفاعل بين الاثنين الذي يصعب الحصول عليه بشكل صحيح. Boost::Iostreams تم إنجاز بعض الأعمال لك، كما أنه يوفر خطافات للمرشحات والسلاسل، لذلك أعتقد أنه سيكون من المفيد تنفيذه بهذه الطريقة.

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

mmap يبدو وكأنه السحر

أخذ الحالة حيث تم تخزين الملف مؤقتًا بالكامل بالفعل1 كخط الأساس2, mmap قد يبدو الأمر كذلك إلى حد كبير سحر:

  1. mmap لا يتطلب سوى استدعاء نظام واحد (من المحتمل) لتعيين الملف بأكمله، وبعد ذلك لن تكون هناك حاجة إلى المزيد من مكالمات النظام.
  2. mmap لا يتطلب نسخة من بيانات الملف من kernel إلى مساحة المستخدم.
  3. mmap يسمح لك بالوصول إلى الملف "كذاكرة"، بما في ذلك معالجته بأي حيل متقدمة يمكنك القيام بها ضد الذاكرة، مثل النقل التلقائي للمترجم، SIMD الجوهرية، والجلب المسبق، وإجراءات التحليل المحسنة في الذاكرة، وOpenMP، وما إلى ذلك.

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

حسنًا، يمكن ذلك.

mmap ليس سحرًا في الواقع لأن ...

لا يزال mmap يعمل لكل صفحة

التكلفة الخفية الأولية ل mmap ضد read(2) (وهو حقًا استدعاء النظام المماثل على مستوى نظام التشغيل لـ كتل القراءة) هل هذا مع mmap ستحتاج إلى القيام "ببعض العمل" لكل صفحة بدقة 4K في مساحة المستخدم، على الرغم من أنها قد تكون مخفية بواسطة آلية خطأ الصفحة.

على سبيل المثال تنفيذ نموذجي فقط mmapسيحتاج الملف بأكمله إلى حدوث خطأ، لذا فإن 100 جيجابايت / 4K = 25 مليون خطأ لقراءة ملف بحجم 100 جيجابايت.الآن، سيكون هؤلاء أخطاء بسيطة, ، لكن 25 مليار صفحة من الأخطاء لن تتم بالسرعة الفائقة.من المحتمل أن تكون تكلفة الخطأ البسيط في حدود 100 نانو في أفضل الأحوال.

يعتمد mmap بشكل كبير على أداء TLB

الآن، يمكنك المرور MAP_POPULATE ل mmap لإخباره بإعداد جميع جداول الصفحات قبل العودة، لذلك يجب ألا تكون هناك أخطاء في الصفحة أثناء الوصول إليها.الآن، يواجه هذا مشكلة صغيرة تتمثل في أنه يقرأ أيضًا الملف بأكمله في ذاكرة الوصول العشوائي (RAM)، وهو ما سينفجر إذا حاولت تعيين ملف بحجم 100 جيجابايت - ولكن دعنا نتجاهل ذلك في الوقت الحالي3.النواة تحتاج إلى القيام به العمل لكل صفحة لإعداد جداول الصفحات هذه (تظهر كوقت kernel).وهذا في نهاية المطاف يمثل تكلفة كبيرة في mmap ويتناسب مع حجم الملف (أي أنه لا يصبح أقل أهمية نسبيًا مع نمو حجم الملف)4.

أخيرًا، حتى في مساحة المستخدم، فإن الوصول إلى مثل هذا التعيين ليس مجانيًا تمامًا (مقارنة بالمخازن المؤقتة الكبيرة للذاكرة التي لا تنشأ من ملف قائم على mmap) - حتى بعد إعداد جداول الصفحات، فإن كل وصول إلى صفحة جديدة سيؤدي، من الناحية النظرية، إلى فقدان TLB.منذ mmapإن استخدام ملف يعني استخدام ذاكرة التخزين المؤقت للصفحة وصفحاتها بدقة 4K، فإنك تتحمل هذه التكلفة مرة أخرى 25 مليون مرة لملف بحجم 100 جيجابايت.

الآن، تعتمد التكلفة الفعلية لأخطاء TLB هذه بشكل كبير على الجوانب التالية على الأقل لجهازك:(أ) ما عدد كيانات 4K TLB الموجودة لديك وكيفية أداء بقية أعمال التخزين المؤقت للترجمة (ب) مدى جودة تعامل الجلب المسبق للأجهزة مع TLB - على سبيل المثال، هل يمكن للجلب المسبق أن يؤدي إلى سير الصفحة؟(ج) مدى سرعة ومدى توازي جهاز السير على الصفحة.في معالجات Intel x86 المتطورة الحديثة، تكون أجهزة التنقل بين الصفحات قوية جدًا بشكل عام:يوجد ما لا يقل عن مسارين متوازيين للصفحة، ويمكن أن يحدث السير على الصفحة بالتزامن مع التنفيذ المستمر، ويمكن أن يؤدي الجلب المسبق للأجهزة إلى تشغيل السير على الصفحة.وبالتالي فإن تأثير TLB على أ تدفق تحميل القراءة منخفض إلى حد ما - وغالبًا ما يؤدي مثل هذا التحميل بشكل مشابه بغض النظر عن حجم الصفحة.الأجهزة الأخرى عادة ما تكون أسوأ بكثير!

read() يتجنب هذه المخاطر

ال read() syscall، وهو ما يكمن بشكل عام وراء استدعاءات النوع "قراءة الحظر" المقدمة على سبيل المثال، في C وC++ واللغات الأخرى، له عيب أساسي واحد يعرفه الجميع جيدًا:

  • كل read() يجب أن يقوم استدعاء N بايت بنسخ N بايت من kernel إلى مساحة المستخدم.

من ناحية أخرى، فإنه يتجنب معظم التكاليف المذكورة أعلاه - فأنت لا تحتاج إلى تعيين 25 مليون صفحة بدقة 4K في مساحة المستخدم.يمكنك عادة malloc مخزن مؤقت واحد صغير في مساحة المستخدم، وأعد استخدامه بشكل متكرر لجميع احتياجاتك read المكالمات.على جانب النواة، لا توجد مشكلة تقريبًا مع صفحات 4K أو فقدان TLB نظرًا لأنه عادةً ما يتم تعيين ذاكرة الوصول العشوائي بأكملها بشكل خطي باستخدام بضع صفحات كبيرة جدًا (على سبيل المثال، صفحات بسعة 1 جيجابايت على x86)، لذلك تتم تغطية الصفحات الأساسية في ذاكرة التخزين المؤقت للصفحة بكفاءة عالية في مساحة النواة.

لذا، لديك أساسًا المقارنة التالية لتحديد أيهما أسرع لقراءة واحدة لملف كبير:

هل العمل الإضافي لكل صفحة يشير ضمنيًا إلى mmap نهج أكثر تكلفة من العمل لكل بايت لنسخ محتويات الملف من kernel إلى مساحة المستخدم ضمنيًا باستخدام read()?

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

على وجه الخصوص، mmap يصبح النهج أسرع نسبيًا عندما:

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

...بينما ال read() يصبح النهج أسرع نسبيًا عندما:

  • ال read() يتمتع syscall بأداء نسخ جيد.على سبيل المثال، جيد copy_to_user الأداء على جانب النواة.
  • تمتلك النواة طريقة فعالة (بالنسبة لأرض المستخدم) لتعيين الذاكرة، على سبيل المثال، باستخدام بضع صفحات كبيرة فقط مع دعم الأجهزة.
  • تحتوي النواة على مكالمات نظام سريعة وطريقة للاحتفاظ بإدخالات kernel TLB عبر مكالمات النظام.

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

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

  • إضافة خطأ حول، الموصوفة أعلاه، والتي تساعد حقا mmap حالة بدون MAP_POPULATE.
  • إضافة المسار السريع copy_to_user طرق في arch/x86/lib/copy_user_64.S, ، على سبيل المثال، باستخدام REP MOVQ عندما يكون سريعًا، مما يساعد حقًا read() قضية.

التحديث بعد Spectre وMeltdown

أدت عمليات التخفيف من آثار ثغرات Spectre وMeltdown إلى زيادة تكلفة استدعاء النظام بشكل كبير.في الأنظمة التي قمت بقياسها، ارتفعت تكلفة استدعاء النظام "لا تفعل شيئًا" (وهو تقدير للحمل الإجمالي الكامل لاستدعاء النظام، بصرف النظر عن أي عمل فعلي يتم إنجازه بواسطة المكالمة) من حوالي 100 نانو ثانية على النظام النموذجي نظام Linux الحديث إلى حوالي 700 نانو ثانية.علاوة على ذلك، اعتمادًا على نظامك، فإن عزل جدول الصفحات يمكن أن يكون للإصلاح المخصص لـ Meltdown تأثيرات إضافية في اتجاه المصب بصرف النظر عن تكلفة استدعاء النظام المباشر بسبب الحاجة إلى إعادة تحميل إدخالات TLB.

كل هذا يعتبر عيب نسبي ل read() الأساليب القائمة بالمقارنة مع mmap الأساليب القائمة، منذ ذلك الحين read() يجب أن تقوم الأساليب بإجراء استدعاء نظام واحد لكل قيمة من البيانات "بحجم المخزن المؤقت".لا يمكنك زيادة حجم المخزن المؤقت بشكل تعسفي لاستهلاك هذه التكلفة نظرًا لأن استخدام المخازن المؤقتة الكبيرة عادةً ما يكون أداءه أسوأ نظرًا لأنك تتجاوز حجم L1 وبالتالي تعاني باستمرار من فقدان ذاكرة التخزين المؤقت.

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


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

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

3 يمكنك التغلب على ذلك، على سبيل المثال، بالتسلسل mmapفي نوافذ ذات حجم أصغر، على سبيل المثال 100 ميغابايت.

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

أنا آسف لأن بن كولينز فقد رمز مصدر mmap الخاص بالنوافذ المنزلقة.سيكون من الجيد أن يكون ذلك في Boost.

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

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

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

كل هذا يعمل بشكل جيد في نظام التشغيل Windows أيضًا باستخدام CreateFileMapping() وMapViewOfFile() (وGetSystemInfo() للحصول على SYSTEM_INFO.dwAlllocationGranularity --- وليس SYSTEM_INFO.dwPageSize).

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

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

أوافق على أن الإدخال/الإخراج لملف mmap'd سيكون أسرع، ولكن أثناء قياس الكود، ألا يجب أن يكون المثال المضاد قليلا المحسن؟

كتب بن كولينز:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

أود أن أقترح أيضًا تجربة:

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

علاوة على ذلك، يمكنك أيضًا محاولة جعل حجم المخزن المؤقت بنفس حجم صفحة واحدة من الذاكرة الافتراضية، في حالة أن 0x1000 ليس بحجم صفحة واحدة من الذاكرة الافتراضية على جهازك...لا يزال الإدخال/الإخراج لملف IMHO mmap'd يفوز، ولكن هذا من شأنه أن يجعل الأمور أقرب.

ربما يتعين عليك معالجة الملفات مسبقًا، بحيث يكون كل سجل في ملف منفصل (أو على الأقل يكون حجم كل ملف قادرًا على استخدام mmap).

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

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

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

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

أعتقد أن أعظم شيء في mmap هو إمكانية القراءة غير المتزامنة مع:

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

المشكلة هي أنني لا أستطيع العثور على MAP_FLAGS المناسب لإعطاء تلميح بضرورة مزامنة هذه الذاكرة من الملف في أسرع وقت ممكن.آمل أن يعطي MAP_POPULATE التلميح الصحيح لـ mmap (أي.لن يحاول تحميل كافة المحتويات قبل العودة من المكالمة، ولكنه سيفعل ذلك بشكل غير متزامن.مع Feed_data).على الأقل يعطي نتائج أفضل مع هذه العلامة حتى أن الدليل ينص على أنه لا يفعل شيئًا بدون MAP_PRIVATE منذ 2.6.23.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top