STL ، المُخصص ، ذاكرتها المشتركة وخصائصها



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



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



يبدو أن الخطابات تزامنت ، وماذا في ذلك؟ على أقل تقدير ، يمكنك استخدام المؤشرات المطلقة وهذا يوفر عليك الكثير من المتاعب. سيصبح من الممكن العمل مع سلاسل C ++ والحاويات التي تم إنشاؤها من الذاكرة المشتركة.



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



المقدمة



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



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



لنلقِ نظرة على بعض أمثلة الاستخدام.



  • “shared memory” MS SQL. (~10...15%)
  • Mysql Windows “shared memory”, .
  • Sqlite WAL-. , . (chroot).
  • PostgreSQL fork - . , .





    .1 PostgreSQL ()


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



يتطلب إثبات المفهوم مهمة ذات مغزى ضئيل :



  • يوجد تناظرية لـ std :: map <std :: string، std :: string> موجودة في الذاكرة المشتركة
  • لدينا عمليات N تضيف / تغير القيم بشكل غير متزامن ببادئة تتوافق مع رقم العملية (على سبيل المثال: key_1_ ... للعملية رقم 1)
  • نتيجة لذلك ، يمكننا التحكم في النتيجة النهائية


لنبدأ بأبسط شيء - نظرًا لأن لدينا خريطة std :: string و std :: ، فنحن بحاجة إلى مخصص STL خاص.



المخصص STL



لنفترض أن هناك xalloc / xfree وظائف للعمل مع الذاكرة المشتركة كما النظير من malloc / الحرة . في هذه الحالة ، يبدو المُخصص كما يلي:



template <typename T>
class stl_buddy_alloc
{
public:
	typedef T                 value_type;
	typedef value_type*       pointer;
	typedef value_type&       reference;
	typedef const value_type* const_pointer;
	typedef const value_type& const_reference;
	typedef ptrdiff_t         difference_type;
	typedef size_t            size_type;
public:
	stl_buddy_alloc() throw()
	{	// construct default allocator (do nothing)
	}
	stl_buddy_alloc(const stl_buddy_alloc<T> &) throw()
	{	// construct by copying (do nothing)
	}
	template<class _Other>
	stl_buddy_alloc(const stl_buddy_alloc<_Other> &) throw()
	{	// construct from a related allocator (do nothing)
	}

	void deallocate(pointer _Ptr, size_type)
	{	// deallocate object at _Ptr, ignore size
		xfree(_Ptr);
	}
	pointer allocate(size_type _Count)
	{	// allocate array of _Count elements
		return (pointer)xalloc(sizeof(T) * _Count);
	}
	pointer allocate(size_type _Count, const void *)
	{	// allocate array of _Count elements, ignore hint
		return (allocate(_Count));
	}
};


هذا يكفي لربط std :: map & std :: string عليه




template <typename _Kty, typename _Ty>
class q_map : 
    public std::map<
        _Kty, 
        _Ty, 
        std::less<_Kty>, 
        stl_buddy_alloc<std::pair<const _Kty, _Ty> > 
    >
{ };

typedef std::basic_string<
        char, 
        std::char_traits<char>, 
        stl_buddy_alloc<char> > q_string


قبل التعامل مع وظائف xalloc / xfree المُعلنة ، والتي تعمل مع المُخصص فوق الذاكرة المشتركة ، يجدر فهم الذاكرة المشتركة نفسها.



ذكريات مشتركه



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



شبابيك



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



    HANDLE hMapFile = CreateFileMapping(
    	INVALID_HANDLE_VALUE,     // use paging file
    	NULL,                     // default security
    	PAGE_READWRITE,           // read/write access
    	(alloc_size >> 32)        // maximum object size (high-order DWORD)
    	(alloc_size & 0xffffffff),// maximum object size (low-order DWORD)
    	"Local\\SomeData");       // name of mapping object


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



    HANDLE hMapFile = OpenFileMapping(
    	FILE_MAP_ALL_ACCESS,      // read/write access
    	FALSE,                    // do not inherit the name
    	"Local\\SomeData");       // name of mapping object
  • أنت الآن بحاجة إلى إنشاء مقطع يشير إلى الشاشة النهائية



    void *hint = (void *)0x200000000000ll;
    unsigned char *shared_ptr = (unsigned char*)MapViewOfFileEx(
    	hMapFile,                 // handle to map object
    	FILE_MAP_ALL_ACCESS,      // read/write permission
    	0,                        // offset in map object (high-order DWORD)
    	0,                        // offset in map object (low-order DWORD)
    	0,                        // segment size,
    	hint);                    // 
    


    يعني حجم المقطع 0 أنه سيتم استخدام الحجم الذي تم إنشاء العرض به ، مع مراعاة التحول.



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


لينكس



كل شيء هو نفسه هنا.



  • إنشاء كائن الذاكرة المشتركة



      int fd = shm_open(
                     “/SomeData”,               //  ,   /
                     O_CREAT | O_EXCL | O_RDWR, // flags,  open
                     S_IRUSR | S_IWUSR);        // mode,  open
    
      ftruncate(fd, alloc_size);
    


    ftruncate . shm_open /dev/shm/. shmget\shmat SysV, ftok (inode ).




  • int fd = shm_open(“/SomeData”, O_RDWR, 0);




  •   void *hint = (void *)0x200000000000ll;
      unsigned char *shared_ptr = (unsigned char*) = mmap(
                       hint,                      // 
                       alloc_size,                // segment size,
                       PROT_READ | PROT_WRITE,    // protection flags
                       MAP_SHARED,                // sharing flags
                       fd,                        // handle to map object
                       0);                        // offset
    


    hint.




بخصوص التلميح ، ما هي القيود على قيمته؟ في الواقع ، هناك أنواع مختلفة من القيود.



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





الشكل 2 حالة صفحات 4K ، مأخوذة هنا



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



في AMD64 ، تبدو الصورة مختلفة قليلاً.





الشكل 3 AMD64 ، صفحات 4K ، مأخوذة من هنا



يحتوي CR3 الآن على 40 بتًا مهمًا بدلاً من 20 سابقًا ، في شجرة من 4 مستويات من الصفحات ، يقتصر العنوان الفعلي على 52 بت بينما يقتصر العنوان الافتراضي على 48 بت.



وفقط (بدءًا من) الهندسة المعمارية المصغرة Ice Lake (Intel) يُسمح باستخدام 57 بتًا من العنوان الظاهري (ولا يزال 52 ماديًا) عند العمل مع جدول صفحات من 5 مستويات.



حتى الآن ، تحدثنا فقط عن Intel / AMD. فقط للتغيير ، في بنية Aarch64 ، يمكن أن يتكون جدول الصفحات من 3 أو 4 مستويات ، مما يسمح باستخدام 39 أو 48 بت في العنوان الظاهري ، على التوالي ( 1 ).



ثانيا، قيود البرامج. Microsoft ، على وجه الخصوص ، تفرض (44 بت حتى 8.1 / Server12 ، 48 تبدأ من) تلك الموجودة على خيارات نظام تشغيل مختلفة بناءً على ، من بين أمور أخرى ، اعتبارات التسويق.



بالمناسبة ، 48 رقمًا ، أي 65 ألف مرة كل 4 جيجابايت ، ربما في مثل هذه الأماكن المفتوحة يوجد دائمًا ركن حيث يمكنك التمسك بتلميحك.



مخصص الذاكرة المشتركة



في البداية. يجب أن يعيش المخصص على الذاكرة المشتركة المخصصة ، ووضع جميع بياناته الداخلية هناك.



ثانيا. نحن نتحدث عن أداة اتصال بين العمليات ، وأي تحسينات مرتبطة باستخدام TLS ليست ذات صلة.



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



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



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



في المجموع ، مع الأخذ في الاعتبار كل ما سبق وأيضًا نظرًا لوجود مخصص مباشر بطريقة التوائم في متناول اليد (تم تقديمه من قِبل Alexander Artyushin ، تمت مراجعته قليلاً) ، لم يكن الاختيار صعبًا.



دعنا نترك وصف تفاصيل التنفيذ حتى أوقات أفضل ، الآن الواجهة العامة مثيرة للاهتمام:



class BuddyAllocator {
public:
	BuddyAllocator(uint64_t maxCapacity, u_char * buf, uint64_t bufsize);
	~BuddyAllocator(){};

	void *allocBlock(uint64_t nbytes);
	void freeBlock(void *ptr);
...
};


المدمر تافه لأنه لا ينتزع BuddyAllocator أي موارد دخيلة.



الاستعدادات النهائية



نظرًا لأن كل شيء موجود في الذاكرة المشتركة ، يجب أن يكون لهذه الذاكرة رأس. بالنسبة للاختبار لدينا ، يبدو هذا العنوان كما يلي:



struct glob_header_t {
	//     magic
	uint64_t magic_;
	// hint     
	const void *own_addr_;
	//  
	BuddyAllocator alloc_;
	// 
	std::atomic_flag lock_;
	//   
	q_map<q_string, q_string> q_map_;

	static const size_t alloc_shift = 0x01000000;
	static const size_t balloc_size = 0x10000000;
	static const size_t alloc_size = balloc_size + alloc_shift;
	static glob_header_t *pglob_;
};
static_assert (
    sizeof(glob_header_t) < glob_header_t::alloc_shift, 
    "glob_header_t size mismatch");

glob_header_t *glob_header_t::pglob_ = NULL;


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



    glob_header_t::pglob_ = (glob_header_t *)shared_ptr;
    
    new (&glob_header_t::pglob_->alloc_)
            qz::BuddyAllocator(
                    //  
                    glob_header_t::balloc_size,
                    //  
                    shared_ptr + glob_header_t::alloc_shift,
                    //   
                    glob_header_t::alloc_size - glob_header_t::alloc_shift;
    
    new (&glob_header_t::pglob_->q_map_) 
                    q_map<q_string, q_string>();
    
    glob_header_t::pglob_->lock_.clear();
    
  • عملية الاتصال بالذاكرة المشتركة تجعل كل شيء جاهزًا
  • الآن لدينا كل ما نحتاجه للاختبارات باستثناء دالتي xalloc / xfree



    void *xalloc(size_t size)
    {
    	return glob_header_t::pglob_->alloc_.allocBlock(size);
    }
    void xfree(void* ptr)
    {
    	glob_header_t::pglob_->alloc_.freeBlock(ptr);
    }
    


يبدو أننا نستطيع البدء.



تجربة



الاختبار نفسه بسيط للغاية:



for (int i = 0; i < 100000000; i++)
{
        char buf1[64];
        sprintf(buf1, "key_%d_%d", curid, (i % 100) + 1);
        char buf2[64];
        sprintf(buf2, "val_%d", i + 1);

        LOCK();

        qmap.erase(buf1); //   
        qmap[buf1] = buf2;

        UNLOCK();
}


Curid هو رقم العملية / الخيط ، العملية التي خلقت الذاكرة المشتركة لا تحتوي على أي تجعيد ، لكن لا يهم الاختبار.

تختلف Qmap و LOCK / UNLOCK باختلاف الاختبارات.



دعونا نجري بعض الاختبارات



  1. THR_MTX - تطبيق متعدد مؤشرات الترابط ، المزامنة تمر عبر std :: recursive_mutex ،

    qmap - global std :: map <std :: string ، std :: string>
  2. THR_SPN هو تطبيق متعدد مؤشرات الترابط ، والمزامنة تمر عبر spinlock:



    std::atomic_flag slock;
    ..
    while (slock.test_and_set(std::memory_order_acquire));  // acquire lock
    slock.clear(std::memory_order_release);                 // release lock


    qmap - الأمراض المنقولة جنسياً العالمية :: خريطة <std :: string، std :: string>
  3. PRC_SPN - عدة عمليات جارية ، والمزامنة تمر عبر spinlock:



    while (glob_header_t::pglob_->lock_.test_and_set(              // acquire lock
            std::memory_order_acquire));                          
    glob_header_t::pglob_->lock_.clear(std::memory_order_release); // release lock
    qmap - glob_header_t :: pglob _-> q_map_
  4. PRC_MTX - عدة عمليات جارية ، تمر المزامنة عبر كائن المزامنة المسمى .



    qmap - glob_header_t :: pglob _-> q_map_


النتائج (نوع الاختبار مقابل عدد العمليات / الخيوط):

1 2 4 8 السادس عشر
THR_MTX 1'56 ' 5'41 ' 7'53 ' 51'38 " 185'49
THR_SPN 1'26 '' 7'38 " 25'30 " 103'29 ' 347'04 ''
PRC_SPN 1'24 ' 7'27 ' 24'02 " 92'34 " 322'41 ''
PRC_MTX 4'55 ' 13'01 '' 78'14 '' 133'25 '' 357'21 ''


تم إجراء التجربة على كمبيوتر ثنائي المعالج (48 مركزًا) مع Xeon® Gold 5118 2.3 جيجا هرتز ، Windows Server 2016.



مجموع



  • نعم ، من الممكن استخدام كائنات / حاويات STL (المخصصة في الذاكرة المشتركة) من عمليات مختلفة ، بشرط أن تكون مصممة بشكل مناسب.
  • , , PRC_SPN THR_SPN. , BuddyAllocator malloc\free MS ( ).
  • . — + std::mutex . lock-free , .




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



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



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



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



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



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


أخيرا



ما يجب فعله وما لا يجب فعله مع الكائنات التي تم إنشاؤها في الذاكرة المشتركة.



  1. استخدم RTTI . لاسباب واضحة. كائن std :: type_info موجود خارج الذاكرة المشتركة ولا يتوفر عبر العمليات.
  2. استخدم الأساليب الافتراضية. لنفس السبب. لا تتوفر جداول الوظائف الافتراضية والوظائف نفسها عبر العمليات.
  3. إذا تحدثنا عن STL ، يجب تجميع جميع الملفات القابلة للتنفيذ للعمليات التي تشارك الذاكرة بواسطة مترجم واحد بنفس الإعدادات ، ويجب أن تكون STL نفسها هي نفسها.


ملاحظة : بفضل ألكسندر أرتيوشن وديمتري إبتيشيف (دميتريا) للمساعدة في تحضير هذه المقالة.



All Articles