في خادم بمقبسين قد تكلّف القراءة نفسها 80 ns أو 130 ns بحسب المقبس الذي تقيم فيه الصفحة. ويُحدَّد مكان إقامة الصفحة عند أول لمسة لها. يضع هذا الدرس نموذجًا لعقدتين ويبيّن كيف يجعل خيط واحد يهيّئ البيانات نصف العمل بعيدًا ويصلح ذلك بجعل كل خيط يلمس جزأه أولًا.
ذاكرة لها مسافة
للخادم الكبير مقبسا معالج ولكل مقبس وحدات ذاكرة خاصة به. تقرأ النواة ذاكرة مقبسها مباشرة. ولقراءة ذاكرة المقبس الآخر يعبر الطلب رابطًا بين المقبسين وهذا يستغرق وقتًا أطول. هذا الجهاز NUMA. يشكّل كل مقبس وذاكرته عقدة وعقدة الخيط هي حيث تقع نواته. في نموذجنا عقدتان بخيطين لكل منهما: الخيطان 0 و1 على العقدة 0 والخيطان 2 و3 على العقدة 1. الوصول المحلي يكلّف 80 ns والبعيد 130 ns.
print([node_of_thread(thread, 2) for thread in range(4)])for thread_node in (0, 1): print("thread on node", thread_node, [LOCAL_NS if page_node == thread_node else REMOTE_NS for page_node in (0, 1)])
[0, 0, 1, 1] thread on node 0 [80.0, 130.0] thread on node 1 [130.0, 80.0]
كل سطر هو منظور خيط: يدفع 80 ns للصفحات على عقدته و130 ns للصفحات على العقدة الأخرى. الفرق 50 ns يبدو صغيرًا لكنه ينطبق على كل وصول يبلغ الذاكرة.
من يقرر أين تقيم الصفحة
يمنح نظام التشغيل الذاكرة للبرنامج صفحات ويختار عقدة الصفحة لحظة استعمالها أول مرة. القاعدة الافتراضية هي اللمسة الأولى: تذهب الصفحة إلى عقدة الخيط الذي يلمسها أولًا وتبقى هناك. والبديل التشابك الصفحات على العقد. يطبّق الكود أدناه القاعدتين على قائمة قصيرة من الوصولات مكتوبة (خيط، عنوان).
def first_touch_placement(trace, threads_per_node): placement = {} for thread, address in trace: page = address // PAGE_BYTES if page not in placement: placement[page] = node_of_thread(thread, threads_per_node) return placementdef interleaved_placement(trace, n_nodes): return {page: page % n_nodes for page in {address // PAGE_BYTES for _, address in trace}}trace = [(0, 0), (0, 4096), (3, 8192), (1, 4100)]print(first_touch_placement(trace, 2))print(interleaved_placement(trace, 2))
{0: 0, 1: 0, 2: 1}
{0: 0, 1: 1, 2: 0}لمس الصفحتين 0 و1 أولًا الخيط 0 الواقع على العقدة 0 فتذهبان إلى العقدة 0. ولمس الصفحة 2 أولًا الخيط 3 على العقدة 1 فتذهب إلى العقدة 1. والوصول اللاحق للخيط 1 إلى الصفحة 1 لا ينقلها. أما التشابك فيتجاهل من يلمس ماذا: تذهب الصفحة 1 إلى العقدة 1 لمجرد أن 1 modulo 2 يساوي 1.
عدّ الوصولات المحلية والبعيدة
تقسّم مهمة متوازية نموذجية جدولًا إلى أجزاء جزء لكل خيط. ثم يقرأ كل خيط جزأه فقط فيُفترض أن تكون كل صفحة يقرؤها على عقدته. تبني الدوال أدناه نمط الوصول هذا لـ400 سجل على أربعة خيوط وتعدّ الوصولات المحلية والبعيدة وزمنها لثلاثة أنماط وضع. في الأول "single" هيّأ خيط واحد كل البيانات فوضعت اللمسة الأولى كل صفحة على العقدة 0. وفي الثاني هيّأ أصحاب الأجزاء أجزاءهم بأنفسهم. وفي الثالث تتشابك الصفحات.
def access_costs(trace, placement, threads_per_node): local = 0 remote = 0 for thread, address in trace: if placement[address // PAGE_BYTES] == node_of_thread(thread, threads_per_node): local += 1 else: remote += 1 return {"local": local, "remote": remote, "ns": local * LOCAL_NS + remote * REMOTE_NS}def partition_trace(n_records, n_threads, record_bytes): chunk = n_records // n_threads return [(thread, record * record_bytes) for thread in range(n_threads) for record in range(thread * chunk, (thread + 1) * chunk)]work = partition_trace(400, 4, 512)single = first_touch_placement([(0, address) for _, address in work], 2)owners = first_touch_placement(work, 2)spread = interleaved_placement(work, 2)for name, placement in (("single", single), ("owners", owners), ("interleaved", spread)): print(name, access_costs(work, placement, 2))
single {'local': 200, 'remote': 200, 'ns': 42000.0}
owners {'local': 400, 'remote': 0, 'ns': 32000.0}
interleaved {'local': 208, 'remote': 192, 'ns': 41600.0}مع خيط واحد يهيّئ كل شيء يكون نصف الوصولات بعيدًا ويستغرق العمل 42000 ns لـ400 قراءة. وحين يهيّئ أصحاب الأجزاء أجزاءهم لا يكون شيء بعيدًا ويستغرق 32000 ns أي أسرع بنحو 1.3 مرة للبرنامج نفسه تمامًا. ولا يفيد التشابك هنا: فبما أن كل خيط يقرأ جزءًا متجاورًا تضع الصفحات المتشابكة نصف بياناته على العقدة الخطأ. التشابك للبيانات التي يقرؤها كل خيط في كل مكان حيث لا يصلح وضع واحد للجميع.
يقرر خيط رئيسي يخصّص مصفوفة كبيرة ويملؤها قبل تشغيل العمال وضع كل صفحة على عقدته. البرنامج صحيح ولا يُظهر المحلل شيئًا خاطئًا سوى تباطؤ يزداد مع عدد المقابس. دع كل عامل يهيّئ الجزء الذي سيستعمله.
في Linux يوزّع numactl --interleave=all صفحات البرنامج على العقد ويثبّت numactl --cpunodebind و--membind الخيوط والذاكرة على عقد ويبيّن numastat كم وصولًا كان محليًا. وعادةً ما تثبّت أنظمة التداول كل خيط على نواة وتمنحه ذاكرة من عقدته.
ما ستبنيه
يتبع المختبر الدرس: عقدة الخيط وقاعدتا الوضع ونمط وصول مهمة مجزّأة وعدّ الوصولات المحلية والبعيدة والدراسة على سجلات SPY.
- الدالة · السؤالتعتمد على
- node_of_thread(thread, threads_per_node)على أي عقدة يعمل خيط؟القسمة الصحيحة
- first_touch_placement(trace, threads_per_node)أين تضع اللمسة الأولى كل صفحة؟node_of_thread
- interleaved_placement(trace, n_nodes)والتشابك؟حساب بسيط
- partition_trace(n_records, n_threads, record_bytes)ما الوصولات التي يجريها كل خيط؟حساب بسيط
- access_costs(trace, placement, threads_per_node)كم وصولًا محليًا وبعيدًا وكم من الوقت؟node_of_thread
- study(bars)كم يكلّف الوضع الخاطئ على سجلات SPY؟partition_tracefirst_touch_placementaccess_costs
الموضوع التالي: مكان إقامة البايتات نصف الحكاية فقط، وكيفية توزيعها مهمة أيضًا. في المخصصات وأحواض الذاكرة يطلب برنامج كثيرًا من الكائنات الصغيرة ويحدد ترتيب توزيع المخصص لها وأماكنه عدد أسطر الذاكرة المخبئية التي يمسّها عبور لاحق.
المختبر: ما الذي يُقيَّم
6 مهام · 100 نقطة- عقدة خيط5 نقطة
- وضع اللمسة الأولى20 نقطة
- الوضع المتشابك10 نقطة
- مهمة مجزّأة15 نقطة
- الوصولات المحلية والبعيدة25 نقطة
- الدراسة على جدول SPY25 نقطة