एक इकाई घटक प्रणाली गेम इंजन में सीपीयू कैश से कैसे लाभ होगा?


15

मैं अक्सर ईसीएस गेम इंजन डॉक्यूमेंटेशन में पढ़ता हूं जो कि सीपीयू कैश का बुद्धिमानी से उपयोग करने के लिए एक अच्छा आर्किटेक्चर है।

लेकिन मैं यह नहीं समझ सकता कि हम सीपीयू कैश से कैसे लाभ उठा सकते हैं।

यदि घटकों को किसी सरणी (या पूल) में सहेजा जाता है, तो सन्निहित स्मृति में, यह CPU कैश BUT का उपयोग करने का एक अच्छा तरीका है, यदि हम घटकों को क्रमिक रूप से पढ़ते हैं।

जब हम सिस्टम का उपयोग करते हैं, तो उन्हें संस्थाओं की सूची की आवश्यकता होती है जो उन संस्थाओं की सूची होती है जिनके पास विशिष्ट प्रकार के घटक होते हैं।

लेकिन ये सूचियाँ घटकों को क्रमबद्ध तरीके से देती हैं, क्रमिक रूप से नहीं।

तो कैश हिट को अधिकतम करने के लिए ईसीएस कैसे डिज़ाइन करें?

संपादित करें:

उदाहरण के लिए, एक भौतिक प्रणाली को उस इकाई के लिए एक संस्थाओं की सूची की आवश्यकता होती है जिसमें रिगिबॉडी और ट्रांसफ़ॉर्म घटक होते हैं (रिगिडबॉडी के लिए एक पूल और ट्रांसफ़ॉर्म घटकों के लिए एक पूल होता है)।

तो अपडेट संस्थाओं के लिए इसका लूप इस तरह होगा:

for (Entity eid in entitiesList) {
    // Get rigid body component
    RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);

    // Get transform component
    Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);

    // Do something with rigid body and transform component
}

समस्या यह है कि Unit1 का RigidBody घटक इसके पूल के सूचकांक 2 पर और उसके पूल के सूचकांक 0 पर Unit1 के Tranform घटक हो सकता है (क्योंकि कुछ संस्थाओं के कुछ घटक हो सकते हैं और दूसरे नहीं और संस्थाओं को जोड़ने या हटाने के कारण हो सकते हैं) / घटक बेतरतीब ढंग से)।

इसलिए, भले ही घटक स्मृति में सन्निहित हों, उन्हें बेतरतीब ढंग से पढ़ा जाता है और इसलिए इसमें अधिक कैश मिस होगा, नहीं?

जब तक लूप में अगले घटकों को प्रीफ़ैच करने का कोई तरीका नहीं है?


क्या आप हमें दिखा सकते हैं कि आप प्रत्येक घटक को कैसे आवंटित कर रहे हैं?
20

पूल में घटकों के स्थानांतरण का प्रबंधन करने के लिए एक साधारण पूल आवंटनकर्ता और एक हैंडल प्रबंधक के साथ (स्मृति में घटकों को सन्निहित रखने के लिए)।
जॉनम्फ

आपका उदाहरण लूप मान लेता है कि घटक अपडेट प्रति इकाई इंटरलेयर्ड हैं। कई मामलों में घटक प्रकार से थोक में घटकों को अपडेट करना संभव है (उदाहरण के लिए, सभी रिगबॉडी घटकों को पहले अपडेट करें, फिर सभी तैयार रिगिडोब डेटा के साथ सभी परिवर्तनों को अपडेट करें, फिर सभी रेंडरिंग डेटा को नए परिवर्तनों के साथ अपडेट करें ...) - यह कैश में सुधार कर सकता है। प्रत्येक घटक अद्यतन के लिए उपयोग करें। मुझे लगता है कि इस प्रकार की संरचना निक विगगिल नीचे सुझा रहे हैं।
DMGregory

यह मेरा उदाहरण है जो बुरा है, वास्तव में, यह भौतिक प्रणाली की तुलना में "समाप्त कठोर शरीर डेटा के साथ सभी परिवर्तनों को अपडेट करता है" है। लेकिन समस्या एक ही रहती है, इन प्रणालियों में (अद्यतन कठोर शरीर के साथ परिवर्तन, परिवर्तन के साथ अद्यतन प्रतिपादन, ...), हमें एक ही समय में एक से अधिक प्रकार के घटक की आवश्यकता होगी।
जॉनम्फ

यकीन नहीं होता कि क्या यह प्रासंगिक भी हो सकता है? gamasutra.com/view/feature/6345/...
DMGregory

जवाबों:


13

मिक वेस्ट का लेख पूर्ण रूप से इकाई घटक डेटा को रैखिक बनाने की प्रक्रिया की व्याख्या करता है। यह टोनी हॉक श्रृंखला के लिए काम करता है, सालों पहले, हमारे पास आज की तुलना में बहुत कम प्रभावशाली हार्डवेयर पर, प्रदर्शन में सुधार करने के लिए। उन्होंने मूल रूप से प्रत्येक विशिष्ट प्रकार के इकाई डेटा (स्थिति, स्कोर और व्हाट्सएप) के लिए वैश्विक, पूर्व-आवंटित सरणियों का उपयोग किया और प्रत्येक सरणी को अपने सिस्टम-वाइड update()फ़ंक्शन के एक अलग चरण में संदर्भित किया । आप मान सकते हैं कि प्रत्येक इकाई के लिए डेटा इन वैश्विक सरणियों में से प्रत्येक में एक ही सरणी सूचकांक में होगा, इसलिए, उदाहरण के लिए, यदि खिलाड़ी पहले बनाया जाता है, तो इसका डेटा [0]प्रत्येक सरणी में हो सकता है ।

कैशे ऑप्टिमाइज़ेशन के लिए और भी अधिक, क्रिस्टर एरिक्सन सी और सी ++ के लिए स्लाइड

थोड़ा और विस्तार देने के लिए, आपको संदर्भ के अच्छे इलाके को सुनिश्चित करने के लिए प्रत्येक प्रकार के डेटा (जैसे स्थिति, xy और z) के अनुसार सन्निहित मेमोरी ब्लॉक (सबसे आसानी से आवंटित) के रूप में उपयोग करने का प्रयास करना चाहिए, प्रत्येक डेटा ब्लॉक का अलग-अलग उपयोग करना। update()लौकिक लोकलिटी के लिए चरण अर्थात कैश को सुनिश्चित करने के लिए कि हार्डवेयर का LRU एल्गोरिथ्म के माध्यम से फ्लश नहीं किया गया है इससे पहले कि आप किसी भी डेटा का पुनः उपयोग करने का इरादा रखते हैं, किसी दिए गए update()कॉल के भीतर । जैसा कि आपने निहित किया है, आप जो नहीं करना चाहते हैं वह अपनी संस्थाओं और घटकों को असतत वस्तुओं के माध्यम से आवंटित करना है new, क्योंकि प्रत्येक इकाई उदाहरण पर विभिन्न प्रकार के डेटा को फिर से दखल दिया जाएगा, संदर्भ के स्थानीयता को कम करना।

यदि आपके पास घटकों (डेटा) के बीच अन्योन्याश्रितियाँ हैं जैसे कि आप बिलकुल अलग-अलग डेटा को इससे जुड़े डेटा से अलग नहीं कर सकते हैं (जैसे। ट्रांसफ़ॉर्म + फ़िज़िक्स, ट्रांसफ़ॉर्म + रेंडरर) तो आप फ़िज़िक्स और रेंडरर सरणियों दोनों में ट्रांसफ़ॉर्म डेटा को दोहराने का विकल्प चुन सकते हैं। , यह सुनिश्चित करता है कि सभी प्रासंगिक डेटा प्रत्येक प्रदर्शन-महत्वपूर्ण संचालन के लिए कैश लाइन की चौड़ाई को फिट करता है।

यह भी याद रखें कि L2 और L3 कैश (यदि आप अपने लक्ष्य प्लेटफ़ॉर्म के लिए इन्हें मान सकते हैं) तो उन समस्याओं को कम करने के लिए बहुत कुछ करें जो L1 कैश को नुकसान पहुंचा सकती हैं, जैसे कि प्रतिबंधात्मक लाइन चौड़ाई। तो एक L1 मिस पर भी, ये सुरक्षा जाल हैं जो अक्सर मुख्य मेमोरी में कॉलआउट को रोकते हैं, जो कि कैशआउट के किसी भी स्तर पर कॉलआउट की तुलना में धीमी गति के आदेश हैं।

डेटा लिखने पर ध्यान दें लेखन मुख्य मेमोरी को कॉल नहीं करता है। डिफ़ॉल्ट रूप से, आज के सिस्टम में राइट-बैक कैशिंग सक्षम है: एक मूल्य लिखना केवल कैश (शुरू में) को लिखता है, मुख्य मेमोरी में नहीं, इसलिए आपको इससे अड़चन नहीं होगी। यह केवल तब होता है जब मुख्य मेमोरी से डेटा का अनुरोध किया जाता है (यह कैश में होने के दौरान नहीं होगा) और बासी है, यह मुख्य मेमोरी कैश से अपडेट किया जाएगा।


1
बस किसी के लिए एक नोट जो C ++ के लिए नया हो सकता है: std::vectorमूल रूप से एक गतिशील रूप से रहने योग्य सरणी है और इसलिए यह भी सन्निहित है (पुराने C ++ संस्करणों में वास्तविक तथ्य और नए C ++ संस्करणों में de jure)। कुछ कार्यान्वयन std::deque"संक्रामक पर्याप्त" (हालांकि Microsoft के नहीं हैं)।
शॉन मिडिलिचच

2
@ जॉनमोफ काफी बस: यदि आपके पास संदर्भ का इलाका नहीं है, तो आपके पास कुछ भी नहीं है। यदि डेटा के दो टुकड़े बारीकी से संबंधित हैं (जैसे स्थानिक और भौतिकी जानकारी), अर्थात वे एक साथ संसाधित होते हैं, तो आपको उन्हें एक एकल घटक के रूप में संकुचित करना पड़ सकता है, इंटरलेय्ड। लेकिन ध्यान रखें कि किसी भी अन्य तर्क (कहते हैं, AI) जो कि स्थानिक डेटा का लाभ उठाता है, उसके बाद स्थानिक डेटा को शामिल नहीं किए जाने के परिणामस्वरूप पीड़ित हो सकता है । तो यह इस बात पर निर्भर करता है कि सबसे अधिक प्रदर्शन की आवश्यकता क्या है (शायद आपके मामले में भौतिकी)। क्या इसका कोई मतलब है?
इंजीनियर

1
@ जॉनमोफ, मैं पूरी तरह से निक के साथ सहमत हूं कि यह कैसे स्मृति में संग्रहीत है, यदि आपके पास दो घटकों के साथ इकाई है जो स्मृति में बहुत दूर हैं, तो आपके पास स्थानीयता नहीं है, उन्हें कैश लाइन में फिट होना होगा।
20

2
@ जॉनमोफ: वास्तव में, मिक वेस्ट का लेख न्यूनतम अंतरनिर्भरता मानता है। तो: निर्भरता कम से कम करें; कैश लाइनों के साथ डेटा को फिर से भरें जहां आप उन निर्भरता को कम नहीं कर सकते ... जैसे कि RigidBody और रेंडर दोनों के साथ ट्रांसफ़ॉर्म शामिल करें ; और कैश लाइनों को फिट करने के लिए, आपको अपने डेटा परमाणुओं को जितना संभव हो उतना कम करने की आवश्यकता हो सकती है ... यह फ़्लोटिंग पॉइंट से फिक्स्ड पॉइंट (4 बाइट्स बनाम 2 बाइट्स) प्रति दशमलव बिंदु मान पर जाकर प्राप्त किया जा सकता है। लेकिन एक तरीका या कोई अन्य, चाहे आप इसे कैसे भी करें, आपके डेटा को अधिकतम प्रदर्शन के लिए, अवधारणा 3d के रूप में कैश लाइन की चौड़ाई को फिट करना होगा।
इंजीनियर

2
@Johnmph। जब भी आप ट्रांसफ़ॉर्म डेटा लिखते हैं, आप बस इसे दोनों ऐरे में लिखते हैं। यह वे नहीं हैं जिनके बारे में आपको चिंतित होने की आवश्यकता है। एक बार जब आप एक लेख भेजते हैं, तो यह उतना ही अच्छा होता है जितना कि किया जाता है। यह पढ़ता है , बाद में अपडेट में, जब आप भौतिक विज्ञान और रेंडरर चलाते हैं, तो सीपीयू के पास और व्यक्तिगत रूप से एक ही कैश लाइन में, तत्काल सभी डेटा तक पहुंच होनी चाहिए । इसके अलावा, अगर आपको वास्तव में एक साथ सभी की आवश्यकता है, तो आप या तो आगे की प्रतिकृति करते हैं या आप सुनिश्चित करते हैं कि भौतिकी, रूपांतरण और एक एकल कैश लाइन फिट है ... 64 बाइट्स आम है और वास्तव में काफी डेटा है! ...
21
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.