OutOfLine - نمط في الذاكرة لتطبيقات C ++ عالية الأداء

أثناء العمل في Headlands Technologies ، كنت محظوظًا بما يكفي لكتابة العديد من الأدوات المساعدة لتبسيط إنشاء كود C ++ عالي الأداء. تقدم هذه المقالة نظرة عامة على واحدة من هذه الأدوات المساعدة ، OutOfLine .


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


قد تبدو النسخة الأولية (المبسطة) كما يلي:


 class UnlinkingFD { std::string path; public: int fd; UnlinkingFD(const std::string& p) : path(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD() { close(fd); unlink(path.c_str()); } UnlinkingFD(const UnlinkingFD&) = delete; }; 

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


ولكن ماذا عن الأداء؟ افترض أن fd يستخدم في كثير من الأحيان ، path فقط عند حذف كائن. يتكون الصفيف الآن من كائنات بحجم 40 بايت ، ولكن غالبًا ما يتم استخدام 4 بايت فقط. هذا يعني أنه سيكون هناك المزيد من الأخطاء في ذاكرة التخزين المؤقت ، لأنك تحتاج إلى "تخطي" 90٪ من البيانات.


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


قد يكون الحل الوسط البسيط هو استبدال std::string بحجم 32 بايت بـ std::unique_ptr<std::string> ، الذي يبلغ حجمه 8 بايت فقط. سيؤدي ذلك إلى تقليل حجم كائننا من 40 بايت إلى 16 بايت ، وهو إنجاز عظيم. لكن هذا الحل لا يزال يفقد استخدام مصفوفات متعددة.


OutOfLine هي أداة تسمح دون التخلي عن RAII بنقل الحقول (الباردة) التي نادراً ما تستخدم خارج الكائن. يتم استخدام OutOfLine كفئة أساسية CRTP ، لذا يجب أن تكون الوسيطة الأولى للقالب فئة فرعية . الوسيطة الثانية هي نوع البيانات (الباردة) التي نادرًا ما تكون مرتبطة بكائن (رئيسي) مستخدم بشكل متكرر.


 struct UnlinkingFD : private OutOfLine<UnlinkingFD, std::string> { int fd; UnlinkingFD(const std::string& p) : OutOfLine<UnlinkingFD, std::string>(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD(); UnlinkingFD(const UnlinkingFD&) = delete; }; 

إذن ما هو هذا الفصل؟


 template <class FastData, class ColdData> class OutOfLine { 

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


  inline static std::map<OutOfLine const*, std::unique_ptr<ColdData>> global_map_; 

يمكن استخدام OutOfLine مع أي نوع من البيانات الباردة ، يتم إنشاء مثيل منها OutOfLine بالكائن الرئيسي تلقائيًا.


  template <class... TArgs> explicit OutOfLine(TArgs&&... args) { global_map_[this] = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } 

تنطوي إزالة الكائن الرئيسي على الإزالة التلقائية للكائن البارد المرتبط:


  ~OutOfLine() { global_map_.erase(this); } 

عند تحريك (مُنشئ نقل / عامل تعيين نقل) للكائن الرئيسي ، سيتم ربط الكائن البارد المقابل تلقائيًا بالكائن الرئيسي الجديد الجديد. نتيجة لذلك ، لا يجب الوصول إلى البيانات الباردة لكائن تم نقله.


  explicit OutOfLine(OutOfLine&& other) { *this = other; } OutOfLine& operator=(OutOfLine&& other) { global_map_[this] = std::move(global_map_[&other]); return *this; } 

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


 OutOfLine(OutOfLine const&) = delete; OutOfLine& operator=(OutOfLine const&) = delete; 

الآن حتى يكون هذا مفيدًا حقًا ، سيكون من الجيد الوصول إلى البيانات الباردة. عند الوراثة من OutOfLine يتلقى الفصل الأساليب cold() وغير الثابتة cold() :


  ColdData& cold() noexcept { return *global_map_[this]; } ColdData const& cold() const noexcept { return *global_map_[this]; } 

يعيدون النوع المناسب من المرجع إلى البيانات الباردة.


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


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


  struct TwoPhaseInit {}; OutOfLine(TwoPhaseInit){} template <class... TArgs> void init_cold_data(TArgs&&... args) { global_map_.find(this)->second = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } void release_cold_data() { global_map_[this].reset(); } 

هذا هو منشئ OutOfLine آخر يمكن استخدامه في الفصول الفرعية ؛ فهو يقبل علامة من النوع TwoPhaseInit . إذا قمت بإنشاء OutOfLine بهذه الطريقة ، فلن تتم تهيئة البيانات الباردة ، وسيبقى الكائن نصف بناء. لإكمال البناء على مرحلتين ، تحتاج إلى استدعاء طريقة init_cold_data (تمرير الوسيطات اللازمة لإنشاء كائن من نوع ColdData ). تذكر أنه لا يمكنك استدعاء .cold() على كائن لم تتم تهيئة بياناته الباردة بعد. قياسا على ذلك ، يمكن حذف البيانات الباردة قبل الموعد المحدد قبل تنفيذ ~OutOfLine المدمر عن طريق استدعاء release_cold_data .


 }; // end of class OutOfLine 

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


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


أعددت اختبارًا حتى تتمكن من رؤية الفرق وتقييمه.


البرنامج النصيالوقت (نانوثانية)
البيانات الباردة في الكائن الرئيسي (الإصدار الأولي)34684547
تم حذف البيانات الباردة تمامًا (أفضل سيناريو)2938327
باستخدام OutOfLine2947645

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


ملاحظة من المترجم


يعمل الرمز الوارد في المقالة على توضيح الفكرة ولا يمثل رمز الإنتاج.

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


All Articles