मेरा प्रश्न यह है कि चूंकि मैं इन मामलों में एक समय में एक रेखीय सरणी का पुनरावृत्ति नहीं कर रहा हूं, क्या मैं तुरंत घटकों को इस तरह आवंटित करने से प्रदर्शन लाभ प्राप्त कर रहा हूं?
संभावना है कि आपको एक "क्षैतिज" चर-आकार ब्लॉक में इकाई से जुड़े घटकों को अलग करने की तुलना में प्रति घटक प्रकार के अलग-अलग "ऊर्ध्वाधर" सरणियों के साथ कम कैश मिक्स मिलेगा।
कारण है, क्योंकि, पहले, "ऊर्ध्वाधर" प्रतिनिधित्व कम मेमोरी का उपयोग करेगा। आपको सम-सामयिक रूप से आवंटित सजातीय सरणियों के लिए संरेखण के बारे में चिंता करने की ज़रूरत नहीं है। गैर-सजातीय प्रकार एक मेमोरी पूल में आवंटित होने के साथ, आपको संरेखण के बारे में चिंता करने की ज़रूरत है क्योंकि सरणी में पहला तत्व दूसरे से बिल्कुल अलग आकार और संरेखण आवश्यकताओं हो सकता है। परिणामस्वरूप आपको अक्सर पैडिंग जोड़ने की आवश्यकता होगी, जैसे कि एक साधारण उदाहरण:
// Assuming 8-bit chars and 64-bit doubles.
struct Foo
{
// 1 byte
char a;
// 1 byte
char b;
};
struct Bar
{
// 8 bytes
double opacity;
// 8 bytes
double radius;
};
मान लीजिए कि हम बिछा करना चाहते हैं Foo
और Bar
और उन्हें स्मृति में सही एक दूसरे के बगल की दुकान:
// Assuming 8-bit chars and 64-bit doubles.
struct FooBar
{
// 1 byte
char a;
// 1 byte
char b;
// 6 bytes padding for 64-bit alignment of 'opacity'
// 8 bytes
double opacity;
// 8 bytes
double radius;
};
अब अलग-अलग मेमोरी क्षेत्रों में फू और बार को स्टोर करने के लिए 18 बाइट्स लेने के बजाय, उन्हें फ्यूज करने के लिए 24 बाइट्स लगते हैं। यदि आप ऑर्डर स्वैप करते हैं तो इससे कोई फर्क नहीं पड़ता:
// Assuming 8-bit chars and 64-bit doubles.
struct BarFoo
{
// 8 bytes
double opacity;
// 8 bytes
double radius;
// 1 byte
char a;
// 1 byte
char b;
// 6 bytes padding for 64-bit alignment of 'opacity'
};
यदि आप एक्सेसिंग पैटर्न में सुधार किए बिना क्रमिक एक्सेस के संदर्भ में अधिक मेमोरी लेते हैं, तो आप आमतौर पर अधिक कैश मिसेज को लाइक करेंगे। इसके शीर्ष पर, एक इकाई से अगली वृद्धि के लिए और एक चर आकार में प्राप्त करने के लिए स्ट्राइड, जिससे आपको एक इकाई से दूसरी में जाने के लिए स्मृति में चर-आकार की छलांग लगानी पड़ती है, यह देखने के लिए कि आपके पास कौन से घटक हैं ' में रुचि रखते हैं
तो एक "ऊर्ध्वाधर" प्रतिनिधित्व का उपयोग करते हुए जैसा कि आप घटक घटकों को संग्रहीत करते हैं, वास्तव में "क्षैतिज" विकल्पों की तुलना में इष्टतम होने की अधिक संभावना है। कहा कि, ऊर्ध्वाधर प्रतिनिधित्व के साथ कैश की समस्या से छूट दी जा सकती है:
जहां तीर केवल संकेत देते हैं कि इकाई "एक घटक" का मालिक है। हम देख सकते हैं कि अगर हम दोनों की संस्थाओं के सभी मोशन और रेंडरिंग घटकों को एक्सेस करने की कोशिश करते हैं, तो हम मेमोरी में सभी जगह कूदते हैं। इस तरह के छिटपुट एक्सेस पैटर्न से आप एक कैश लाइन में डेटा लोड कर सकते हैं, कह सकते हैं, एक मोशन कंपोनेंट, फिर अधिक कंपोनेंट एक्सेस कर सकते हैं और उस पूर्व डेटा को बेदखल कर देते हैं, केवल उसी मैमोरी रीजन को लोड करने के लिए जो पहले से ही दूसरे मोशन के लिए बेदखल था। घटक। तो यह बहुत ही बेकार हो सकता है कि एक ही मेमोरी क्षेत्रों में एक ही बार कैश लाइनों में लोड हो रहा है और घटकों की सूची तक पहुंच प्राप्त कर सकते हैं।
आइए उस गंदगी को थोड़ा साफ करें ताकि हम अधिक स्पष्ट रूप से देख सकें:
ध्यान दें कि यदि आप इस तरह के परिदृश्य का सामना करते हैं, तो यह आमतौर पर खेल शुरू होने के बाद लंबे समय तक होता है, कई घटकों और संस्थाओं को जोड़ने और हटाने के बाद। सामान्य तौर पर जब खेल शुरू होता है, तो आप सभी संस्थाओं और संबंधित घटकों को एक साथ जोड़ सकते हैं, जिस बिंदु पर उनके पास एक स्थानिक, क्रमिक एक्सेस पैटर्न हो सकता है जिसमें अच्छे स्थानिक इलाके हों। हालांकि बहुत सारे निष्कासन और सम्मिलन के बाद, आपको उपरोक्त गड़बड़ जैसा कुछ मिल सकता है।
उस स्थिति को सुधारने का एक बहुत ही आसान तरीका यह है कि आप केवल अपने स्वामित्व वाली संस्था आईडी / इंडेक्स के आधार पर अपने घटकों को मूलांक दें। उस समय आपको कुछ ऐसा मिलता है:
और यह बहुत अधिक कैश-फ्रेंडली एक्सेस पैटर्न है। यह सही नहीं है क्योंकि हम देख सकते हैं कि हमें कुछ रेंडरिंग और मोशन कंपोनेंट्स को यहां-वहां छोड़ना होगा क्योंकि हमारा सिस्टम केवल उन एंटाइटीज़ में दिलचस्पी रखता है जो दोनों के पास हैं उन , और कुछ संस्थाओं में केवल एक मोशन कंपोनेंट होता है और कुछ में केवल एक रेंडरिंग कंपोनेंट होता है , लेकिन आप कम से कम अंत में कुछ सन्निहित घटकों को संसाधित करने में सक्षम हो सकते हैं (व्यवहार में अधिक, आम तौर पर, चूंकि आप अक्सर ब्याज के प्रासंगिक घटकों को संलग्न करते हैं, जैसे आपके सिस्टम में संभवतः अधिक इकाइयां जो एक गति घटक है, की तुलना में एक प्रतिपादन घटक होगा नहीं)।
सबसे महत्वपूर्ण बात यह है कि एक बार जब आप ये छाँट लेते हैं, तो आप डेटा मेमोरी क्षेत्र को कैश लाइन में लोड नहीं कर रहे हैं, केवल एक लूप में फिर से लोड कर सकते हैं।
और इसके लिए कुछ अत्यंत जटिल डिज़ाइन की आवश्यकता नहीं होती है, बस एक रेखीय समय का रेडिक्स सॉर्ट हर अब और फिर से गुजरता है, हो सकता है कि आपके द्वारा सम्मिलित किए जाने के बाद और किसी विशेष घटक प्रकार के लिए घटकों का एक गुच्छा निकाला जाए, जिस बिंदु पर आप इसे चिह्नित कर सकते हैं क्रमबद्ध करने की आवश्यकता है। यथोचित रूप से लागू किया गया मूलांक सॉर्ट (आप इसे समानांतर भी कर सकते हैं, जो मैं करता हूं) मेरे क्वाड-कोर i7 के बारे में 6ms में एक लाख तत्वों को सॉर्ट कर सकता है, जैसा कि यहां पर उदाहरण दिया गया है:
Sorting 1000000 elements 32 times...
mt_sort_int: {0.203000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_sort: {1.248000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_radix_sort: {0.202000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
std::sort: {1.810000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
qsort: {2.777000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
ऊपर एक लाख तत्वों को 32 बार क्रमबद्ध करना है ( memcpy
छँटाई के पहले और बाद के परिणामों के समय सहित )। और मैं मान रहा हूँ कि अधिकांश समय आपके पास वास्तव में एक लाख + घटक नहीं होंगे, इसलिए आपको बहुत आसानी से अब इसमें और बिना किसी ध्यान देने योग्य फ्रेम दर स्टुटर्स के बिना यह पता लगाने में सक्षम होना चाहिए।