कैश की याद आती है और इकाई प्रणालियों में प्रयोज्य


18

हाल ही में मैं अपने ढांचे के लिए एक इकाई प्रणाली पर शोध और कार्यान्वयन कर रहा हूं। मुझे लगता है कि मैंने इसके बारे में अधिकांश लेख, रेडिट और प्रश्न पढ़े, जो मुझे मिल सकते थे, और अब तक मुझे लगता है कि मैं इस विचार को काफी समझ रहा हूं।

हालाँकि, इसने कुल C ++ व्यवहार के बारे में कुछ प्रश्न उठाए, जिस भाषा में मैं इकाई प्रणाली को लागू करता हूं, साथ ही कुछ प्रयोज्य मुद्दे भी।

इसलिए, एक दृष्टिकोण सीधे इकाई में घटकों की एक सरणी को संग्रहीत करना होगा, जो मैंने नहीं किया क्योंकि यह डेटा के माध्यम से पुनरावृत्ति करने पर कैश के इलाके को बर्बाद कर देता है। इस वजह से, मैंने एक सरणी प्रति घटक प्रकार का फैसला किया, इसलिए एक ही प्रकार के सभी घटक स्मृति में सन्निहित हैं, जो त्वरित पुनरावृत्ति के लिए इष्टतम समाधान होना चाहिए।

लेकिन, जब मैं एक वास्तविक गेमप्ले कार्यान्वयन पर एक सिस्टम से उनके साथ कुछ करने के लिए घटक सरणियों को पुनरावृत्त करने के लिए होता हूं, तो मुझे लगता है कि मैं लगभग हमेशा दो या अधिक घटक प्रकारों के साथ काम कर रहा हूं। उदाहरण के लिए, रेंडर सिस्टम वास्तव में रेंडर कॉल करने के लिए एक साथ ट्रांसफॉर्म और मॉडल घटक का उपयोग करता है। मेरा प्रश्न यह है कि चूंकि मैं इन मामलों में एक समय में एक रेखीय सरणी का पुनरावृत्ति नहीं कर रहा हूं, क्या मैं तुरंत घटकों को इस तरह आवंटित करने से प्रदर्शन लाभ प्राप्त कर रहा हूं? क्या यह एक समस्या है जब मैं सी + +, दो अलग-अलग सन्निहित सरणियों में पुनरावृत्ति करता हूं और प्रत्येक चक्र पर दोनों से डेटा का उपयोग करता हूं?

एक और बात जो मैं पूछना चाहता था, वह यह है कि किसी को घटकों या संस्थाओं के संदर्भ में कैसे रखना चाहिए, क्योंकि बहुत ही प्रकृति के घटकों को स्मृति में कैसे रखा जाता है, वे आसानी से पदों को सरणी में बदल सकते हैं या विस्तार के लिए सरणी को फिर से विभाजित किया जा सकता है या सिकुड़ रहा है, मेरे घटक संकेत छोड़ रहा है या अमान्य है। आप इन मामलों को संभालने की सलाह कैसे देते हैं, क्योंकि मैं अक्सर खुद को हर फ्रेम में ट्रांसफ़ॉर्म और अन्य घटकों पर काम करना चाहता हूं और अगर मेरे हैंडल या पॉइंटर्स अमान्य हैं, तो हर फ्रेम को लुकअप करना काफी गड़बड़ है।


4
मैं घटकों को एक सतत मेमोरी में डालने की जहमत नहीं उठाऊंगा लेकिन गतिशील रूप से प्रत्येक घटक के लिए मेमोरी आवंटित करूंगा। सन्निहित स्मृति आपको किसी भी कैश परफ़ॉर्मेंस का लाभ नहीं देती है क्योंकि आप घटकों को वैसे भी बहुत यादृच्छिक क्रम में एक्सेस कर सकते हैं।
जर्ककोल

@Grimshaw यहां पढ़ने के लिए एक दिलचस्प लेख है: हानिकारक.cat-v.org/software/OO_programming/_pdf/…
Raxvan

@ जारको -10 अंक। यह वास्तव में प्रदर्शन को नुकसान पहुंचाता है यदि आप एक सिस्टम कैश फ्रेंडली का निर्माण करते हैं और इसे यादृच्छिक तरीके से एक्सेस करते हैं , तो यह केवल ध्वनि से बेवकूफ है। इसे रैखिक तरीके से एक्सेस करने का बिंदु । ईसीएस और प्रदर्शन लाभ की कला रैखिक तरीके से एक्सेस सी / एस लिखने के बारे में है।
वंद्रा

@Grimshaw कैश बड़ा है तो एक पूर्णांक भूल नहीं है। आपको L1 कैश के कई KB उपलब्ध हैं (और अन्य के MB), यदि आप कुछ भी खतरनाक नहीं करते हैं, तो कैश सिस्टम के अनुकूल होने के दौरान कुछ सिस्टम को एक्सेस करना ठीक रहेगा।
वंद्रा

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

जवाबों:


13

पहले, मैं यह नहीं कहूंगा कि इस मामले में आप अपने उपयोग के मामले के आधार पर बहुत जल्दी अनुकूलन कर रहे हैं। किसी भी मामले में, आपने एक दिलचस्प सवाल पूछा है और जैसा कि मेरे पास खुद के साथ अनुभव है, मैं इसमें वज़न लूंगा। मैं बस यह समझाने की कोशिश करूँगा कि मैंने किस तरह से चीजें खत्म कीं और मुझे रास्ते में क्या मिला।

  • प्रत्येक इकाई में जेनेरिक घटक हैंडल का वेक्टर होता है जो किसी भी प्रकार का प्रतिनिधित्व कर सकता है।
  • प्रत्येक घटक हैंडल को कच्चे T * पॉइंटर उपज के लिए डीरेल किया जा सकता है। *निचे देखो।
  • प्रत्येक घटक प्रकार का अपना पूल है, मेमोरी का एक निरंतर ब्लॉक (मेरे मामले में निश्चित आकार)।

यह ध्यान दिया जाना चाहिए कि नहीं, आप हमेशा एक घटक पूल को पार करने और आदर्श, साफ काम करने में सक्षम नहीं होंगे। वहाँ हैं, जैसा कि आपने कहा है, घटकों के बीच अपरिहार्य लिंक, जिसमें आपको वास्तव में एक समय में एक इकाई चीजों को संसाधित करने की आवश्यकता होती है।

हालांकि, ऐसे मामले हैं (जैसा कि मैंने पाया है) जहां वास्तव में, आप सचमुच एक विशेष घटक प्रकार के लिए लूप के लिए लिख सकते हैं और अपने सीपीयू कैश लाइनों का शानदार उपयोग कर सकते हैं। जो लोग अनभिज्ञ हैं या अधिक जानना चाहते हैं, वे https://en.wikipedia.org/wiki/Locality_of_reference पर एक नज़र डालें । एक ही नोट पर, जब संभव हो, अपने घटक आकार को अपने सीपीयू कैश लाइन के आकार से कम या बराबर रखने की कोशिश करें। मेरी लाइन का आकार 64 बाइट्स था, जो मुझे लगता है कि आम है।

मेरे मामले में, सिस्टम को लागू करने का प्रयास करना इसके लायक था। मैंने दृश्य प्रदर्शन लाभ (पाठ्यक्रम का प्रोफाइल) देखा। आपको अपने लिए तय करने की आवश्यकता होगी कि क्या यह एक अच्छा विचार है। प्रदर्शन में सबसे बड़ा लाभ मैंने 1000+ संस्थाओं में देखा।

एक और बात जो मैं पूछना चाहता था, वह यह है कि किसी को घटकों या संस्थाओं के संदर्भ में कैसे रखना चाहिए, क्योंकि बहुत ही प्रकृति के घटकों को स्मृति में कैसे रखा जाता है, वे आसानी से पदों को सरणी में बदल सकते हैं या विस्तार के लिए सरणी को फिर से विभाजित किया जा सकता है या सिकुड़ रहा है, मेरे घटक संकेत छोड़ रहा है या अमान्य है। आप इन मामलों को संभालने की सलाह कैसे देते हैं, क्योंकि मैं अक्सर खुद को हर फ्रेम में ट्रांसफ़ॉर्म और अन्य घटकों पर काम करना चाहता हूं और अगर मेरे हैंडल या पॉइंटर्स अमान्य हैं, तो हर फ्रेम को लुकअप करना काफी गड़बड़ है।

मैंने व्यक्तिगत रूप से इस मुद्दे को भी हल किया। मैं समाप्त हुआ जहां एक प्रणाली है:

  • प्रत्येक घटक हैंडल एक पूल इंडेक्स का संदर्भ रखता है
  • जब किसी घटक को एक पूल से 'हटा दिया जाता है' या 'हटा दिया' जाता है, तो उस पूल के भीतर अंतिम घटक को स्थानांतरित कर दिया जाता है (शाब्दिक रूप से std :: चाल के साथ) अब नि: शुल्क स्थान पर, या कोई भी नहीं अगर आपने अंतिम घटक को हटा दिया है।
  • जब एक 'स्वैप' होता है, तो मेरे पास एक कॉलबैक होता है जो किसी भी श्रोता को सूचित करता है, ताकि वे किसी भी ठोस बिंदु (जैसे टी *) को अपडेट कर सकें।

* मैंने पाया कि मैं जिस प्रदर्शन प्रणाली की समस्या से जूझ रहा था, उसकी संख्या के साथ उच्च उपयोग कोड के कुछ वर्गों में रनवे पर हमेशा डिरेलमेंट घटक को संभालने की कोशिश की जा रही थी। उसके कारण, अब मैं अपने प्रोजेक्ट के महत्वपूर्ण हिस्सों के प्रदर्शन में कुछ कच्चे टी पॉइंटर्स को बनाए रखता हूं, लेकिन अन्यथा मैं जेनेरिक घटक हैंडल का उपयोग करता हूं, जिसका उपयोग संभव हो। मैं उन्हें ऊपर बताए अनुसार मान्य रखता हूं, कॉलबैक सिस्टम के साथ। आप के रूप में दूर जाने की जरूरत नहीं हो सकती है।

इन सबसे ऊपर, बस चीजों की कोशिश करो। जब तक आपको वास्तविक दुनिया का परिदृश्य नहीं मिलता, तब तक कोई भी व्यक्ति यहां कहता है कि वह केवल एक ही काम कर रहा है, जो आपके लिए उचित नहीं है।

क्या उससे मदद हुई? मैं कुछ भी स्पष्ट करने की कोशिश करूंगा जो अस्पष्ट है। इसके अलावा किसी भी सुधार की सराहना की जाती है।


अपवित्र, यह वास्तव में एक अच्छा जवाब था, और जबकि यह एक चांदी की गोली नहीं हो सकती है, फिर भी किसी को समान डिजाइन विचारों को देखने के लिए अच्छा है। मेरे पास आपके ES में भी आपके कुछ ट्रिक्स लागू हैं, और वे व्यावहारिक लगते हैं। आपका बहुत बहुत धन्यवाद! यदि वे आते हैं तो आगे के विचारों पर टिप्पणी करने के लिए स्वतंत्र महसूस करें।
ग्रिम्सहाव

5

इसका उत्तर देने के लिए:

मेरा प्रश्न यह है कि चूंकि मैं इन मामलों में एक समय में एक रेखीय सरणी का पुनरावृत्ति नहीं कर रहा हूं, क्या मैं तुरंत घटकों को इस तरह आवंटित करने से प्रदर्शन लाभ प्राप्त कर रहा हूं? क्या यह एक समस्या है जब मैं सी + +, दो अलग-अलग सन्निहित सरणियों में पुनरावृत्ति करता हूं और प्रत्येक चक्र पर दोनों से डेटा का उपयोग करता हूं?

नहीं (कम से कम जरूरी नहीं)। कैश नियंत्रक को, ज्यादातर मामलों में, एक से अधिक सन्निहित सरणी से कुशलतापूर्वक पढ़ने से निपटने में सक्षम होना चाहिए। महत्वपूर्ण हिस्सा यह कोशिश करना है कि प्रत्येक सरणी को रैखिक रूप से एक्सेस करना संभव हो।

इसे प्रदर्शित करने के लिए, मैंने एक छोटा बेंचमार्क (सामान्य बेंचमार्क कैविट्स लागू होता है) लिखा।

एक साधारण वेक्टर संरचना के साथ शुरू:

struct float3 { float x, y, z; };

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

यदि मैंने डेटा को यादृच्छिक रूप से एक्सेस किया है, तो प्रदर्शन 10 और 20 के बीच एक कारक का सामना करना पड़ा।

समय (10,000,000 तत्व)

रैखिक पहुंच

  • अलग-अलग एरेज़ 0.21
  • interleaved स्रोत 0.21 s
  • interleaved स्रोत और परिणाम 0.48s

रैंडम एक्सेस (असंगत random_shuffle)

  • 2.42 से अलग सरणियाँ
  • interleaved स्रोत 4.43 s
  • interleaved स्रोत और परिणाम 4.00 s

स्रोत (विजुअल स्टूडियो 2013 के साथ संकलित):

#include <Windows.h>
#include <vector>
#include <algorithm>
#include <iostream>

struct float3 { float x, y, z; };

float3 operator+( float3 const &a, float3 const &b )
{
    return float3{ a.x + b.x, a.y + b.y, a.z + b.z };
}

struct Both { float3 a, b; };

struct All { float3 a, b, res; };


// A version without any indirection
void sum( float3 *a, float3 *b, float3 *res, int n )
{
    for( int i = 0; i < n; ++i )
        *res++ = *a++ + *b++;
}

void sum( float3 *a, float3 *b, float3 *res, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        res[*index] = a[*index] + b[*index];
}

void sum( Both *both, float3 *res, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        res[*index] = both[*index].a + both[*index].b;
}

void sum( All *all, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        all[*index].res = all[*index].a + all[*index].b;
}

class PerformanceTimer
{
public:
    PerformanceTimer() { QueryPerformanceCounter( &start ); }
    double time()
    {
        LARGE_INTEGER now, freq;
        QueryPerformanceCounter( &now );
        QueryPerformanceFrequency( &freq );
        return double( now.QuadPart - start.QuadPart ) / double( freq.QuadPart );
    }
private:
    LARGE_INTEGER start;
};

int main( int argc, char* argv[] )
{
    const int count = 10000000;

    std::vector< float3 > a( count, float3{ 1.f, 2.f, 3.f } );
    std::vector< float3 > b( count, float3{ 1.f, 2.f, 3.f } );
    std::vector< float3 > res( count );

    std::vector< All > all( count, All{ { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f } } );
    std::vector< Both > both( count, Both{ { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f } } );

    std::vector< int > index( count );
    int n = 0;
    std::generate( index.begin(), index.end(), [&]{ return n++; } );
    //std::random_shuffle( index.begin(), index.end() );

    PerformanceTimer timer;
    // uncomment version to test
    //sum( &a[0], &b[0], &res[0], &index[0], count );
    //sum( &both[0], &res[0], &index[0], count );
    //sum( &all[0], &index[0], count );
    std::cout << timer.time();
    return 0;
}

1
यह कैश इलाके के बारे में मेरे संदेह के साथ बहुत मदद करता है, धन्यवाद!
ग्रिम्साव

सरल अभी तक दिलचस्प जवाब है कि मुझे भी आश्वस्त लगता है :) मुझे यह देखने के लिए दिलचस्पी होगी कि ये परिणाम अलग-अलग आइटम की गणना के लिए कैसे भिन्न होते हैं (यानी, 10,000,000 के बजाय 1000?) या यदि आपके पास मूल्यों के अधिक सरणियों हैं (अर्थात, 3 के तत्व संक्षेप करें) -5 अलग सरणियों और मूल्य एक और अलग सरणी में संग्रहीत)।
असेमानिया

2

संक्षिप्त उत्तर: प्रोफ़ाइल तब अनुकूलित करें।

लंबा जवाब:

लेकिन, जब मैं एक वास्तविक गेमप्ले कार्यान्वयन पर एक सिस्टम से उनके साथ कुछ करने के लिए घटक सरणियों को पुनरावृत्त करने के लिए होता हूं, तो मुझे लगता है कि मैं लगभग हमेशा दो या अधिक घटक प्रकारों के साथ काम कर रहा हूं।

क्या यह एक समस्या है जब मैं सी + +, दो अलग-अलग सन्निहित सरणियों में पुनरावृत्ति करता हूं और प्रत्येक चक्र पर दोनों से डेटा का उपयोग करता हूं?

C ++ कैश मिस के लिए ज़िम्मेदार नहीं है, क्योंकि यह किसी भी प्रोग्रामिंग भाषा के लिए लागू होता है। यह आधुनिक सीपीयू वास्तुकला कैसे काम करता है, इसके साथ करना है।

आपकी समस्या एक अच्छा उदाहरण हो सकती है जिसे प्री-मैच्योर ऑप्टिमाइज़ेशन कहा जा सकता है ।

मेरी राय में आप प्रोग्राम मेमोरी एक्सेस पैटर्न को देखे बिना कैश लोकेलिटी के लिए बहुत जल्दी अनुकूलित हो गए। लेकिन बड़ा सवाल यह है कि क्या आपको वास्तव में अनुकूलन के इस प्रकार (संदर्भ के इलाके) की आवश्यकता है?

एग्नर के कोहरे से पता चलता है कि आपको अपने आवेदन को प्रोफाइल करने से पहले अनुकूलित नहीं करना चाहिए और / या यह सुनिश्चित करना चाहिए कि अड़चनें कहां हैं। (यह सभी उनके उत्कृष्ट मार्गदर्शक में उल्लिखित है। नीचे लिंक दें)

यह जानना उपयोगी है कि कैश का आयोजन कैसे किया जाता है यदि आप ऐसे प्रोग्राम बना रहे हैं जिनमें गैर-अनुक्रमिक पहुंच के साथ बड़ी डेटा संरचनाएं हैं और आप कैश कॉन्ट्रेशन को रोकना चाहते हैं। यदि आप अधिक प्रत्यक्ष दिशानिर्देशों से संतुष्ट हैं तो आप इस अनुभाग को छोड़ सकते हैं।

दुर्भाग्यवश आपने जो किया वह वास्तव में यह माना गया था कि प्रति घटक एक घटक प्रकार आवंटित करने से आपको बेहतर प्रदर्शन मिलेगा, जबकि वास्तव में आपके पास अधिक कैश मिस या यहां तक ​​कि कैश विवाद भी हो सकता है।

आपको निश्चित रूप से उनके उत्कृष्ट सी ++ अनुकूलन गाइड को देखना चाहिए ।

एक और बात, जिसके बारे में मैं पूछना चाहता था, वह यह है कि किसी को घटकों या संस्थाओं के संदर्भ में कैसे रखा जाना चाहिए, क्योंकि यह बहुत ही प्रकृति का है कि घटकों को स्मृति में कैसे रखा जाता है।

व्यक्तिगत रूप से मैं सबसे अधिक इस्तेमाल किए गए घटकों को एक ही मेमोरी ब्लॉक में एक साथ आवंटित करूंगा, इसलिए उनके पास "पास" पते हैं। उदाहरण के लिए एक सरणी इस तरह दिखाई देगी:

[{ID0 Transform Model PhysicsComp }{ID10 Transform Model PhysicsComp }{ID2 Transform Model PhysicsComp }..] और फिर वहां से अनुकूलन करना शुरू करें यदि प्रदर्शन "काफी अच्छा" नहीं था।


मेरा प्रश्न निहितार्थ के बारे में था कि मेरी वास्तुकला प्रदर्शन पर हो सकती है, बिंदु को अनुकूलित करने के लिए नहीं था, लेकिन आंतरिक रूप से चीजों को व्यवस्थित करने का एक तरीका चुनना था । जिस तरह से यह अंदर हो रहा है, उसके बावजूद मैं चाहता हूं कि मेरा गेम कोड एक सजातीय तरीके से इसके साथ बातचीत करना चाहता है अगर मैं बाद में बदलना चाहता हूं। यदि डेटा संग्रहीत करने के बारे में अतिरिक्त सुझाव दे सकते हैं तो भी आपका उत्तर अच्छा था। Upvoted।
ग्रिम्साव

मैं जो देखता हूं, उसमें घटकों को संग्रहीत करने के तीन मुख्य तरीके हैं, सभी को एक सरणी में प्रति इकाई, सभी को एक साथ व्यक्तिगत सरणियों में टाइप किया जाता है, और यदि मैं सही तरीके से समझाता हूं, तो आप अलग-अलग एंटिटीज को बड़े सरणी में स्टोर करने का सुझाव देते हैं, और प्रति इकाई, इसके सभी घटक एक साथ हैं?
ग्रिमशॉ

@Grimshaw जैसा कि मैंने उत्तर में उल्लेख किया है, आपके आर्किटेक्चर को सामान्य आवंटन पैटर्न की तुलना में बेहतर परिणाम देने की गारंटी नहीं है। चूंकि आप वास्तव में अपने अनुप्रयोगों के एक्सेस पैटर्न को नहीं जानते हैं। इस तरह के अनुकूलन आमतौर पर कुछ अध्ययन / सबूत के बाद किए जाते हैं। मेरे सुझाव के संबंध में, एक ही मेमोरी में संबंधित घटकों और विभिन्न स्थानों में अन्य घटकों को संग्रहीत करें। यह सभी या कुछ नहीं के बीच का एक मध्य मैदान है। फिर भी, मैं अभी भी यह अनुमान लगाता हूं कि यह अनुमान लगाना कठिन है कि आपके आर्किटेक्चर ने दिए गए परिणामों को प्रभावित किया होगा कि कितनी परिस्थितियाँ खेल में आती हैं।
concept3d

डाउनवॉटर देखभाल की व्याख्या करने के लिए? बस मेरे उत्तर में समस्या को इंगित करें। बेहतर अभी तक एक बेहतर जवाब दे।
कॉन्सेप्ट 3 डी

1

मेरा प्रश्न यह है कि चूंकि मैं इन मामलों में एक समय में एक रेखीय सरणी का पुनरावृत्ति नहीं कर रहा हूं, क्या मैं तुरंत घटकों को इस तरह आवंटित करने से प्रदर्शन लाभ प्राप्त कर रहा हूं?

संभावना है कि आपको एक "क्षैतिज" चर-आकार ब्लॉक में इकाई से जुड़े घटकों को अलग करने की तुलना में प्रति घटक प्रकार के अलग-अलग "ऊर्ध्वाधर" सरणियों के साथ कम कैश मिक्स मिलेगा।

कारण है, क्योंकि, पहले, "ऊर्ध्वाधर" प्रतिनिधित्व कम मेमोरी का उपयोग करेगा। आपको सम-सामयिक रूप से आवंटित सजातीय सरणियों के लिए संरेखण के बारे में चिंता करने की ज़रूरत नहीं है। गैर-सजातीय प्रकार एक मेमोरी पूल में आवंटित होने के साथ, आपको संरेखण के बारे में चिंता करने की ज़रूरत है क्योंकि सरणी में पहला तत्व दूसरे से बिल्कुल अलग आकार और संरेखण आवश्यकताओं हो सकता है। परिणामस्वरूप आपको अक्सर पैडिंग जोड़ने की आवश्यकता होगी, जैसे कि एक साधारण उदाहरण:

// 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छँटाई के पहले और बाद के परिणामों के समय सहित )। और मैं मान रहा हूँ कि अधिकांश समय आपके पास वास्तव में एक लाख + घटक नहीं होंगे, इसलिए आपको बहुत आसानी से अब इसमें और बिना किसी ध्यान देने योग्य फ्रेम दर स्टुटर्स के बिना यह पता लगाने में सक्षम होना चाहिए।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.