हाल ही में, हमारे ब्लॉग पर NUMA सिस्टम पर
एक लेख दिखाई दिया, और मैं लिनक्स में अपने अनुभव को साझा करके विषय को जारी रखना चाहूंगा। आज मैं बात करूंगा कि क्या होता है अगर NUMA में मेमोरी का उपयोग करना गलत है और प्रदर्शन काउंटरों का उपयोग करके इस तरह की समस्या का निदान कैसे किया जाए।
तो, चलो एक सरल उदाहरण से शुरू करते हैं:

यह एक सरल परीक्षण है जो एक लूप में एक सरणी के तत्वों को प्रस्तुत करता है। इसे एक दोहरे सॉकेट सर्वर पर कई थ्रेड्स में चलाएं, जिसमें प्रत्येक सॉकेट में क्वाड-कोर प्रोसेसर है। नीचे एक ग्राफ दिया गया है, जिस पर हम थ्रेड्स की संख्या के आधार पर प्रोग्राम के निष्पादन समय को देखते हैं:

हम देखते हैं कि आठ थ्रेड्स पर निष्पादन समय चार थ्रेड्स की तुलना में केवल 1.16 गुना कम है, हालांकि जब दो से चार थ्रेड्स पर स्विच करते हैं, तो प्रदर्शन में वृद्धि अधिक होती है। अब एक सरल कोड परिवर्तन करते हैं: सरणी आरंभीकरण से पहले एक समानांतर निर्देश जोड़ें:

और रन बार फिर से इकट्ठा करें:

और अब, आठ थ्रेड्स पर लगभग 2 बार प्रदर्शन सुधार हुआ था। इस प्रकार, हमारे आवेदन लगभग पूरी तरह से धाराओं की पूरी सीमा पर हो जाते हैं।
तो, आइए जानें कि क्या हुआ? आरंभीकरण चक्र का एक सरल समानांतरकरण लगभग दो गुना वृद्धि कैसे हुई? NUMA समर्थन के साथ एक दोहरे प्रोसेसर सर्वर पर विचार करें:

प्रत्येक चार-कोर प्रोसेसर को एक निश्चित मात्रा में भौतिक मेमोरी दी जाती है, जिसके साथ यह एक एकीकृत मेमोरी कंट्रोलर और डेटा बस के माध्यम से संचार करता है। प्रोसेसर + मेमोरी के इस तरह के एक गुच्छा को नोड या नोड कहा जाता है। NUMA- सिस्टम (नॉन यूनिफ़ॉर्म मेमोरी एक्सेस) में दूसरे नोड की मेमोरी तक पहुँच अपने नोड की मेमोरी तक पहुँच की तुलना में अधिक समय लेती है। जब कोई एप्लिकेशन पहली बार मेमोरी एक्सेस करता है, तो मेमोरी के वर्चुअल पेज भौतिक लोगों को सौंपे जाते हैं। लेकिन लिनक्स में चलने वाले एनयूएमए-सिस्टमों में, इस प्रक्रिया की अपनी विशिष्टताएं हैं: जिन भौतिक पृष्ठों को आभासी लोगों को सौंपा जाएगा उन्हें नोड पर आवंटित किया जाता है जिसके साथ पहली पहुंच हुई। यह तथाकथित "पहली-स्पर्श नीति" है। यानी यदि पहले नोड से कोई मेमोरी एक्सेस की गई थी, तो इस मेमोरी के वर्चुअल पेज भौतिक लोगों पर प्रदर्शित किए जाएंगे, जिन्हें पहले नोड पर भी आवंटित किया जाएगा। इसलिए, डेटा को सही ढंग से प्रारंभ करना महत्वपूर्ण है, क्योंकि एप्लिकेशन प्रदर्शन इस बात पर निर्भर करेगा कि डेटा नोड्स को कैसे सौंपा गया है। यदि हम पहले उदाहरण के बारे में बात करते हैं, तो पूरे सरणी को एक नोड पर आरंभीकृत किया गया था, जिसके कारण सभी डेटा को पहले नोड पर ठीक किया गया था, जिसके बाद इस सरणी का आधा भाग दूसरे नोड द्वारा पढ़ा गया था, और इसके कारण खराब प्रदर्शन हुआ।
चौकस पाठक को पहले से ही सवाल पूछा जाना चाहिए: "क्या मैलोडॉक के माध्यम से मेमोरी आवंटन पहली पहुंच नहीं है?"। विशेष रूप से, इस मामले में - नहीं। यहाँ बात है: जब लिनक्स में मेमोरी के बड़े ब्लॉक आवंटित करते हैं, तो ग्लिबक
मी एलोकेशन फ़ंक्शन (साथ ही कॉलोक और रियललॉक), डिफ़ॉल्ट रूप से, एमएमएपी कर्नेल सर्विस फ़ंक्शन को कॉल करता है। यह सेवा फ़ंक्शन केवल आवंटित मेमोरी की मात्रा के बारे में नोट्स बनाता है, लेकिन भौतिक आवंटन केवल उनके लिए पहली पहुंच पर होता है। यह तंत्र पेज-फॉल्ट और कॉपी-ऑन-राइट रुकावटों (अपवादों) के साथ-साथ मैपिंग के माध्यम से "शून्य" पृष्ठ पर लागू किया जाता है। जो लोग विवरण में रुचि रखते हैं वे लिनक्स कर्नेल को समझना पुस्तक को पढ़ सकते हैं। सामान्य तौर पर, एक स्थिति तब संभव होती है जब ग्लिबेक
सी आबंटित फ़ंक्शन इसे "nullify" करने के लिए पहली मेमोरी एक्सेस का प्रदर्शन करेगा। लेकिन फिर, यह तब होगा जब कॉलोक उपयोगकर्ता को पहले से मुक्त मेमोरी को ढेर (ढेर) पर वापस करने का फैसला करता है, और ऐसी मेमोरी पहले से ही भौतिक पृष्ठों पर मौजूद होगी। इसलिए, अनावश्यक पहेलियों से बचने के लिए, तथाकथित NUMA- अवगत मेमोरी मैनेजर (उदाहरण के लिए, TCMalloc) का उपयोग करने की सिफारिश की जाती है, लेकिन यह एक और विषय है।
और अब आइए इस लेख के मुख्य प्रश्न का उत्तर दें: "मुझे कैसे पता चलेगा कि एप्लिकेशन NUMA सिस्टम में मेमोरी के साथ सही तरीके से काम करता है?"। यह सवाल हमेशा हमारे लिए पहला और सबसे महत्वपूर्ण होगा जब ऑपरेटिंग सिस्टम की परवाह किए बिना NUMA समर्थन वाले सर्वरों के लिए अनुप्रयोगों का उपयोग करना।
इस प्रश्न का उत्तर देने के लिए, हमें एक वीटीएन एम्पलीफायर की आवश्यकता है जो दो प्रदर्शन काउंटरों के लिए घटनाओं को पढ़ सके: OFFCORE_RESPONSE_0.ANY_REQUEST.LOCAL_DRAM और OFFCORE_RESPONSE_0.ANY_REQUEST.REMOTE_DRAM। पहला काउंटर उन सभी अनुरोधों की संख्या को गिनाता है जिनके लिए डेटा उसके नोड की रैम में पाया गया था, और दूसरा - दूसरे नोड की मेमोरी में। बस मामले में, आप अभी भी कैश के लिए काउंटर इकट्ठा कर सकते हैं: OFFCORE_RESPONSE_0.ANY_REQUEST.LOCAL_CACHE और OFFCORE_RESPONSE_0.ANY_REPEST.REMOTE_CACHE। अचानक यह पता चलता है कि डेटा मेमोरी में नहीं है, लेकिन एक विदेशी नोड पर प्रोसेसर के कैश में?
इसलिए, हम VTune के तहत आठ थ्रेड्स में इनिशियलाइज़ेशन को समानांतर किए बिना अपना एप्लिकेशन चलाएंगे और उपरोक्त गणना के लिए ईवेंट की संख्या की गणना करेंगे:

हम देखते हैं कि सीपीयू 0 पर चलने वाला धागा मुख्य रूप से इसके नोड के साथ काम करता है। यद्यपि समय-समय पर, किसी कारण से इस कर्नेल पर vmlinux मॉड्यूल अन्य लोगों के नोड्स में देखा गया। लेकिन सीपीयू 1 पर स्ट्रीम ने इसके विपरीत किया: सभी अनुरोधों के केवल 0.13% के लिए डेटा अपने नोड में पाया गया था। यहाँ मुझे समझाना चाहिए कि गुठली नोड्स को कैसे सौंपी जाती है। गुठली 0,2,4,6 पहले नोड से संबंधित है, और गुठली 1,3,5,7 दूसरे से संबंधित है। टोपोलॉजी को अंकतालिका उपयोगिता का उपयोग करके पाया जा सकता है:
numactl - हार्डवेयर
उपलब्ध: 2 नोड्स (0-1)
नोड 0 सीपीस: 0 2 4 6नोड 0 आकार: 12277 एमबी
नोड 0 नि: शुल्क: 10853 एमबी
नोड 1 सीपी: 1 3 5 7नोड 1 आकार: 12287 एमबी
नोड 1 मुफ्त: 11386 एमबी
नोड दूरी:
नोड ० १
0: 10 20
1: 20 10
कृपया ध्यान दें कि तार्किक संख्याएं यहां सूचीबद्ध हैं, वास्तव में, कोर 0,2,4,6 एक क्वाड-कोर प्रोसेसर से संबंधित हैं, और कोर 1,3,5,7 दूसरे से संबंधित हैं।
आइए अब समानांतर आरंभीकरण के साथ एक उदाहरण के लिए काउंटरों के मूल्य को देखें:

तस्वीर लगभग सही है, हम देखते हैं कि सभी गुठली मुख्य रूप से अपने नोड्स के साथ काम करती हैं। विदेशी नोड्स के लिए कॉल सभी अनुरोधों का आधा प्रतिशत से अधिक नहीं बनाते हैं, सीपीयू के अपवाद के साथ 6. यह कर्नेल सभी अनुरोधों का लगभग 4.5% विदेशी नोड को भेजता है। क्योंकि एक विदेशी नोड तक पहुंचने में अपने आप से 2 गुना अधिक समय लगता है, फिर 4.5% ऐसे अनुरोध प्रदर्शन को काफी कम नहीं करते हैं। इसलिए, हम कह सकते हैं कि अब एप्लिकेशन मेमोरी के साथ सही तरीके से काम करता है।
इस प्रकार, इन काउंटरों का उपयोग करके आप हमेशा यह निर्धारित कर सकते हैं कि क्या NUMA सिस्टम के लिए एप्लिकेशन को गति देना संभव है। व्यवहार में, मेरे पास ऐसे मामले थे जब सही डेटा आरंभीकरण ने 2 बार अनुप्रयोगों को गति दी, और कुछ अनुप्रयोगों में मुझे सभी चक्रों को समानांतर करना पड़ा, एक नियमित एसएमपी प्रणाली के लिए प्रदर्शन को थोड़ा कम करना।
उन लोगों के लिए जो 4.5% में रुचि रखते हैं, मैं आगे जाने का सुझाव देता हूं। मेमोरी सिस्टम की गतिविधि का विश्लेषण करने के लिए नेहेलम प्रोसेसर और इसके वंशज काउंटरों का एक समृद्ध समूह है। ये सभी काउंटर OFFCORE_RESPONSE के नाम से शुरू होते हैं। यह भी लग सकता है कि उनमें से बहुत सारे हैं। लेकिन अगर आप ध्यान से देखें, तो आप देख सकते हैं कि वे सभी समग्र अनुरोधों और प्रतिक्रियाओं के संयोजन हैं। प्रत्येक समग्र अनुरोध या प्रतिक्रिया में मूल अनुरोध और प्रतिक्रियाएं होती हैं जो एक बिटमास्क द्वारा निर्दिष्ट होती हैं।
समग्र अनुरोधों और प्रतिक्रियाओं के लिए बिट मास्क के मूल्य निम्नलिखित हैं:

इस तरह से नेफेल प्रोसेसर में OFFCORE_RESPONSE_0 का काउंटर बनता है:

उदाहरण के लिए, हमारे OFFCORE_RESPONSE_0.ANY_REQUEST.REMOTE_DRAM के उदाहरण देखें। इसमें एक मिश्रित अनुरोध any_REQUEST और एक समग्र प्रतिक्रिया REMOTE_DRAM शामिल हैं। Any_REQUEST अनुरोध में xxFF का मान है, जिसका अर्थ है सभी घटनाओं पर नज़र रखना: डेटा को पढ़ने से "मांग पर" (बिट 0, तालिका में डिमांड डेटा Rd) अनुदेश कैश के प्रीफ़ेटर्स (बिट 6, पीएफ इफ़ैट) और बाकी "ट्रिफ़ल्स" (बिट 7) , अन्य)। प्रतिक्रिया REMOTE_DRAM का मान 20xx है, जिसका अर्थ है ट्रैकिंग अनुरोध जिसके लिए डेटा केवल दूसरे नोड (बिट 13 L3_MISS_REMOTE_DRAM) की स्मृति में पाया गया था। इन काउंटरों पर सभी जानकारी Intel.com डॉक्यूमेंट "Intel 64 और IA-32 आर्किटेक्चर ऑप्टिमाइज़ेशन रेफरेंस मैनुअल", "B.2.3.5 मापने वाली कोर मेमोरी एक्सेस लेटेंसी" पर पाई जा सकती है।
यह समझने के लिए कि कौन अपने अनुरोधों को किसी और के नोड में भेजता है, आपको किसी भी अनुरोध को समग्र अनुरोधों में बदलने की आवश्यकता है: DEMAND_DATA_RD, DEMAND_RFO, DEMAND_IFETCH, COREWB, PF_DATA_RD, PF_RFO, PF_IFETCH, OTHER और अन्य घटनाएँ। इस प्रकार, "अपराधी" पाया गया:
OFFCORE_RESPONSE_0.PREFETCH.REMOTE_DRAM
सीपीयू 0: 6405
सीपीयू 1: 597190
सीपीयू 2: 2503
सीपीयू 3: 229271
सीपीयू 4: 2035
सीपीयू 5: 190549
cpu 6: 19364266सीपीयू 7: 228027
लेकिन 6 वें कर्नेल पर प्रीफ़ैचर ने विदेशी नोड में क्यों देखा, जबकि अन्य कर्नेल के प्रीफ़ैक्टर ने अपने नोड्स के साथ काम किया था? तथ्य यह है कि समानांतर इनिशियलाइज़ेशन के साथ उदाहरण को चलाने से पहले, मैंने अतिरिक्त रूप से गुठली के लिए धागे के तंग बंधन स्थापित किए हैं:
एक्सपोर्ट KMP_AFFINITY = ग्रैन्युलैरिटी = फाइन, प्रोलिस्ट = [0,2,4,6,1,3,5,7], स्पष्ट, क्रिया
./a.outOMP: जानकारी # 204: KMP_AFFINITY: x2APIC आईडी को डिकोड करना।
OMP: Info # 202: KMP_AFFINITY: ग्लोबल कॉउपिड लीफ 11 इंफॉर्मेशन का उपयोग करते हुए एफिनिटी सक्षम
OMP: जानकारी # 154: KMP_AFFINITY: आरंभिक OS खरीद सेट सम्मानित: {0,1,2,3,4,6,6,000}
OMP: जानकारी # 156: KMP_AFFINITY: 8 उपलब्ध OS procs
OMP: जानकारी # 157: KMP_AFFINITY: समान टोपोलॉजी
OMP: जानकारी # 179: KMP_AFFINITY: 2 पैकेज x 4 कोर / pkg x 1 थ्रेड / कोर (8 कुल कोर)
OMP: जानकारी # 206: KMP_AFFINITY: भौतिक थ्रेड मैप के लिए OS खरीद:
OMP: Info # 171: KMP_AFFINITY: OS 0 0 पैकेज 0 कोर 0 के लिए मैप करता है
OMP: Info # 171: KMP_AFFINITY: OS 0 पैकेज 1 के पैकेज के लिए 4 नक्शे खरीदता है
OMP: जानकारी # 171: KMP_AFFINITY: 0 कोर 2 पैकेज के लिए OS 2 नक्शे खरीदता है
OMP: Info # 171: KMP_AFFINITY: 0 कोर 3 पैकेज के लिए OS 6 नक्शे खरीदता है
OMP: जानकारी # 171: KMP_AFFINITY: OS 1 पैकेज 1 कोर 0 पर खरीदता है
OMP: Info # 171: KMP_AFFINITY: OS 1 पैकेज 1 के लिए 5 नक्शे खरीदता है
OMP: जानकारी # 171: KMP_AFFINITY: OS 1 पैकेज 2 कोर 2 के लिए 3 नक्शे खरीदता है
OMP: Info # 171: KMP_AFFINITY: OS 1 पैकेज 3 कोर 3 के लिए 7 नक्शे खरीदता है
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक थ्रेड 0 ओएस सेट के लिए बाध्य {0}
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक थ्रेड 1 के लिए OS खरीद सेट {2}
OMP: जानकारी # 147: KMP_AFFINITY: OS के लिए आंतरिक धागा 2 बाउंड्री सेट {4}
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक खरीद 3 OS के लिए बाध्य सेट {6}
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक थ्रेड 4 ओएस खरीद के लिए बाध्य {1}
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक थ्रेड 5 ओएस सेट के लिए बाध्य {3}
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक थ्रेड 6 ओएस खरीद के लिए बाध्य {5}
OMP: जानकारी # 147: KMP_AFFINITY: आंतरिक थ्रेड 7 ओएस सेट के लिए बाध्य {7}
इस बंधन के अनुसार, पहले चार धागे पहले नोड पर काम करते हैं, और दूसरे चार धागे दूसरे पर काम करते हैं। इससे पता चलता है कि 6 वां कोर पहला नोड (0,2,4,6) से संबंधित अंतिम कोर है। आमतौर पर, प्रीफ़ैचर हमेशा मेमोरी को उसके आगे डाउनलोड करने की कोशिश करता है, जो उस दिशा में (या पीछे उस दिशा पर निर्भर करता है जिसमें प्रोग्राम मेमोरी तक पहुंचता है)। हमारे मामले में, छठे कोर के प्रीफ़ैचर ने उस मेमोरी को अपलोड किया जो उस समय से आगे थी, जिसके साथ थ्रेड इंटरनल थ्रेड 3 उस पल में काम कर रहा था। यह वह जगह है जहाँ एक विदेशी नोड के लिए कॉल हुआ, क्योंकि सामने वाला मेमोरी आंशिक रूप से एक विदेशी नोड के पहले कोर से संबंधित था (1,3) , 5.7)। और इसने 4.5% कॉल की उपस्थिति को एक विदेशी नोड के लिए प्रेरित किया।
नोट: परीक्षण कार्यक्रम इंटेल कंपाइलर द्वारा –no-vec विकल्प के साथ वेक्टर कोड के बजाय स्केलर कोड प्राप्त करने के लिए संकलित किया गया था। यह सिद्धांत की समझ को सुविधाजनक बनाने के लिए "सुंदर डेटा" प्राप्त करने के लिए किया गया था।