डायनेमिक मेमोरी आवंटन और मेमोरी प्रबंधन


17

एक औसत गेम में, दृश्य में सैकड़ों या शायद हजारों वस्तुएं होती हैं। क्या बंदूक की गोली (गोलियों) सहित सभी वस्तुओं के लिए मेमोरी आवंटित करना पूरी तरह से सही है, गतिशील रूप से डिफ़ॉल्ट नए () के माध्यम से ?

क्या मुझे गतिशील आवंटन के लिए कोई मेमोरी पूल बनाना चाहिए , या क्या इससे परेशान होने की कोई जरूरत नहीं है? क्या होगा अगर लक्ष्य मंच मोबाइल डिवाइस हैं?

क्या मोबाइल गेम में मेमोरी मैनेजर की जरूरत है ? धन्यवाद।

प्रयुक्त भाषा: C ++; वर्तमान में विंडोज के तहत विकसित किया गया है, लेकिन बाद में पोर्ट किए जाने की योजना है।


कौनसी भाषा?
काइलोटन

@ किलोटन: जिस भाषा का इस्तेमाल किया गया है: सी ++ वर्तमान में विंडोज के तहत विकसित की गई थी लेकिन बाद में पोर्ट किए जाने की योजना थी।
Bunkai.Satori

जवाबों:


23

एक औसत खेल में, दृश्य में सैकड़ों या शायद हजारों मोटापे होते हैं। क्या यह सभी वस्तुओं के लिए मेमोरी आवंटित करने के लिए पूरी तरह से सही है, जिसमें गन शॉट्स (गोलियां), गतिशील रूप से डिफ़ॉल्ट नए () के माध्यम से शामिल हैं?

यह वास्तव में निर्भर करता है कि आप "सही" से क्या मतलब है। यदि आप शब्द का शाब्दिक अर्थ लेते हैं (और निहित डिजाइन की शुद्धता की किसी भी अवधारणा को अनदेखा करते हैं) तो हाँ, यह पूरी तरह से स्वीकार्य है। आपका कार्यक्रम संकलित करेगा और ठीक चलेगा।

यह उप-आशावादी प्रदर्शन कर सकता है, लेकिन यह अभी भी एक shippable, मजेदार खेल होने के लिए काफी अच्छा प्रदर्शन कर सकता है।

क्या मुझे गतिशील आवंटन के लिए कोई मेमोरी पूल बनाना चाहिए, या क्या इससे परेशान होने की कोई जरूरत नहीं है? क्या होगा अगर लक्ष्य मंच मोबाइल डिवाइस हैं?

प्रोफाइल और देखें। सी ++ में, उदाहरण के लिए, ढेर पर गतिशील रूप से आवंटन आमतौर पर एक "धीमा" ऑपरेशन होता है (इसमें उपयुक्त आकार के ब्लॉक की तलाश में हीप के माध्यम से चलना शामिल होता है)। C # में, यह आमतौर पर एक बहुत तेज़ ऑपरेशन है क्योंकि इसमें वेतन वृद्धि की तुलना में थोड़ा अधिक शामिल है। स्मृति आवंटन, विमोचन पर विमोचन, वगैरह के साथ अलग-अलग भाषा कार्यान्वयन में अलग-अलग प्रदर्शन विशेषताएँ होती हैं।

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

क्या मोबाइल गेम में मेमोरी मैनेजर की जरूरत है? धन्यवाद।

फिर से, प्रोफ़ाइल और देखें। क्या आपका खेल अब ठीक चल रहा है? फिर आपको चिंता करने की आवश्यकता नहीं हो सकती है।

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

विशेष रूप से, अपने मूल उदाहरण में आपने "गोलियों" का हवाला दिया, जो कुछ ऐसा बनते हैं जो अक्सर बनते और नष्ट होते हैं - क्योंकि कई गेम में बहुत सारी गोलियां शामिल होती हैं, और गोलियां तेजी से चलती हैं और इस तरह उनके जीवनकाल के अंत तक पहुंचती हैं (और अक्सर हिंसक!)। इसलिए उनके लिए और उनके जैसी वस्तुओं (जैसे कि एक कण प्रणाली में कण) को लागू करना आमतौर पर दक्षता हासिल कर सकता है और संभवतः पूल आवंटन का उपयोग करना शुरू करने के लिए पहली जगह होगी।

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

उदाहरण के लिए यदि आप एक मेमोरी मैनेजर को एक ऐसी चीज मानते हैं जो सिर्फ नए / डिलीट / फ्री / मॉलोक / जो भी कॉल को इंटरसेप्ट करता है और आपको कितनी मेमोरी आवंटित करता है, जो आप लीक करते हैं, वगैरह - तो वह उपयोगी हो सकता है खेल के लिए टूल जबकि यह आपके डिबग लीक्स और आपके इष्टतम मेमोरी पूल साइज़ और इतने पर ट्यून करने में आपकी मदद के लिए है।


माना। एक तरह से कोड जो आपको बाद में चीजों को बदलने की अनुमति देता है। यदि संदेह है, तो बेंचमार्क या प्रोफाइल।
axel22

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

+1। उत्कृष्ट विस्तार। लगभग हर प्रदर्शन प्रश्न का उत्तर हमेशा "प्रोफ़ाइल और देखें" होता है। पहले सिद्धांतों से प्रदर्शन के कारण इन दिनों हार्डवेयर बहुत जटिल है। आपको डेटा चाहिए।
उदार

@ शानदार: आपकी टिप्पणी के लिए धन्यवाद। तो लक्ष्य खेल काम और stalbe बना रही है। विकास के बीच में प्रदर्शन के बारे में बहुत अधिक चिंता करने की आवश्यकता नहीं है। यह सब खेल के पूरा होने के बाद तय किया जा सकता है।
Bunkai.Satori

मुझे लगता है कि यह C # के आवंटन समय का अनुचित प्रतिनिधित्व है- उदाहरण के लिए, प्रत्येक C # आवंटन में सिंक ब्लॉक, ऑब्जेक्ट का आवंटन आदि भी शामिल है। इसके अलावा, C ++ में हीप को आवंटन और मुक्त होने के दौरान केवल संशोधन की आवश्यकता होती है, जबकि C को संग्रह की आवश्यकता होती है। ।
डेडएमजी

7

मेरे पास जोश के उत्कृष्ट उत्तर को जोड़ने के लिए बहुत कुछ नहीं है, लेकिन मैं इस पर टिप्पणी करूंगा:

क्या मुझे गतिशील आवंटन के लिए कोई मेमोरी पूल बनाना चाहिए, या क्या इससे परेशान होने की कोई जरूरत नहीं है?

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


अच्छा जोड़ के लिए +1। हां, आप सही हैं, सरल गेम तत्वों जैसे: बुलेट, कण, प्रभाव को प्रबंधित करने का यह एक अच्छा तरीका है। विशेष रूप से उन लोगों के लिए, गतिशील रूप से मेमोरी आवंटित करने की आवश्यकता नहीं होगी।
बंकई। कटोरी

3

क्या बंदूक की गोली (गोलियों) सहित सभी वस्तुओं के लिए स्मृति को आवंटित करना पूरी तरह से सही है, गतिशील रूप से डिफ़ॉल्ट नए () के माध्यम से?

नहीं बिलकुल नहीं। सभी वस्तुओं के लिए कोई स्मृति आवंटन सही नहीं है । ऑपरेटर नया () डायनेमिक एलोकेशन के लिए है, यानी यह तभी उचित है, जब आपको एलोकेशन के लिए डायनेमिक होने की जरूरत हो, या तो ऑब्जेक्ट का लाइफटाइम डायनामिक हो या क्योंकि ऑब्जेक्ट का टाइप डायनेमिक हो। यदि ऑब्जेक्ट का प्रकार और जीवनकाल सांख्यिकीय रूप से ज्ञात है, तो आपको इसे सांख्यिकीय रूप से आवंटित करना चाहिए।

बेशक, आपके पास अपने आवंटन पैटर्न के बारे में अधिक जानकारी है, इन आवंटन को तेजी से विशेषज्ञ आवंटनकर्ताओं जैसे कि ऑब्जेक्ट पूल के माध्यम से किया जा सकता है। लेकिन, ये अनुकूलन हैं और आपको उन्हें केवल तभी बनाना चाहिए जब वे आवश्यक हो।


अच्छे उत्तर के लिए +1। इसलिए सामान्य करने के लिए, सही दृष्टिकोण होगा: विकास की शुरुआत में, योजना बनाने के लिए कि किन वस्तुओं को सांख्यिकीय रूप से आवंटित किया जा सकता है। विकास के दौरान, गतिशील रूप से केवल उन वस्तुओं को आवंटित करने के लिए जिन्हें बिल्कुल गतिशील रूप से आवंटित किया जाना है। अंत में, संभव मेमोरी आवंटन प्रदर्शन समस्याओं को प्रोफाइल करने और समायोजित करने के लिए।
बंकई.टोरि

0

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

यहां एक सरल उदाहरण दिया गया है कि आप Foosएक साथ जुड़े हुए तत्वों के साथ छेद के साथ एक सरणी का उपयोग करके बार-बार आवंटन और मुक्त करने से कैसे बच सकते हैं (इसे "आवंटनकर्ता" स्तर के बजाय "कंटेनर" स्तर पर हल कर सकते हैं):

struct FooNode
{
    explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}

    // Stores a 'Foo'.
    Foo element;

    // Points to the next foo available; either the
    // next used foo or the next deleted foo. Can
    // use SoA and hoist this out if Foo doesn't 
    // have 32-bit alignment.
    int next;
};

struct Foos
{
    // Stores all the Foo nodes.
    vector<FooNode> nodes;

    // Points to the first used node.
    int first_node;

    // Points to the first free node.
    int free_node;

    Foos(): first_node(-1), free_node(-1)
    {
    }

    const FooNode& operator[](int n) const
    {
         return data[n];
    }

    void insert(const Foo& element)
    {
         int index = free_node;
         if (index != -1)
         {
              // If there's a free node available,
              // pop it from the free list, overwrite it,
              // and push it to the used list.
              free_node = data[index].next;
              data[index].next = first_node;
              data[index].element = element;
              first_node = index;
         }
         else
         {
              // If there's no free node available, add a 
              // new node and push it to the used list.
              FooNode new_node(element);
              new_node.next = first_node;
              first_node = data.size() - 1;
              data.push_back(new_node);
         }
    }

    void erase(int n)
    {
         // If the node being removed is the first used
         // node, pop it from the used list.
         if (first_node == n)
              first_node = data[n].next;

         // Push the node to the free list.
         data[n].next = free_node;
         free_node = n;
    }
};

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

for (int index = foos.first_node; index != -1; index = foos[index].next)
    // do something with foos[index]

यहां छवि विवरण दर्ज करें

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

यह कहा कि, यह संरचना स्थानिक इलाकों में आपके द्वारा हटाए जाने और बीच से बहुत कुछ / चीजें सम्मिलित करने के बाद कम हो जाती है। उस बिंदु nextपर आप वेक्टर के साथ आगे और पीछे चल सकते हैं, डेटा को उसी अनुक्रमिक ट्रैवर्सल के भीतर कैश लाइन से पूर्व में हटाए गए डेटा को फिर से लोड करना (यह किसी भी डेटा संरचना या आवंटन के साथ अपरिहार्य है जो पुनः प्राप्त करते समय तत्वों को हटाने के बिना निरंतर समय निकालने की अनुमति देता है। निरंतर-समय सम्मिलन के साथ बीच से रिक्त स्थान और समानांतर बिटसेट या removedध्वज की तरह कुछ का उपयोग किए बिना । कैश-फ्रेंडली को पुनर्स्थापित करने के लिए, आप एक कॉपी ctor और स्वैप विधि इस तरह से लागू कर सकते हैं:

Foos(const Foos& other)
{
    for (int index = other.first_node; index != -1; index = other[index].next)
        insert(foos[index].element);
}

void Foos::swap(Foos& other)
{
     nodes.swap(other.nodes):
     std::swap(first_node, other.first_node);
     std::swap(free_node, other.free_node);
}

// ... then just copy and swap:
Foos(foos).swap(foos);

अब नया संस्करण कैश-फ्रेंडली है फिर से ट्रैवर्स के लिए। एक अन्य विधि संरचना में सूचकांक की एक अलग सूची संग्रहीत करती है और उन्हें समय-समय पर सॉर्ट करती है। एक और बिटसेट का उपयोग यह इंगित करने के लिए है कि सूचकांकों का उपयोग क्या किया जाता है। यह हमेशा आपको अनुक्रमिक क्रम में बिटसेट का पता लगाने के लिए होगा (कुशलता से ऐसा करने के लिए, एफएफएस / एफएफजेड का उपयोग करके एक समय में 64-बिट की जांच करें)। बिटसेट सबसे कुशल और गैर-घुसपैठ है, जिसके लिए केवल एक समानांतर बिट प्रति तत्व की आवश्यकता होती है जो इंगित करता है कि कौन से उपयोग किए जाते हैं और जिन्हें 32-बिट nextसूचकांकों की आवश्यकता के बजाय हटा दिया जाता है , लेकिन सबसे अच्छा लिखने के लिए सबसे अधिक समय लगता है (यह नहीं होगा) ट्रैवर्सल के लिए तेज़ रहें यदि आप एक समय में एक बिट की जाँच कर रहे हैं - आपको कब्जे वाले सूचकांकों की सीमाओं का तेजी से निर्धारण करने के लिए एक बार में 32+ बिट्स के बीच एक सेट या परेशान बिट को खोजने के लिए FFS / FFZ की आवश्यकता है)।

यह जुड़ा हुआ समाधान आम तौर पर लागू करने और गैर-दखल देने के लिए सबसे आसान है ( Fooकुछ removedध्वज को संग्रहीत करने के लिए संशोधित करने की आवश्यकता नहीं है ) यदि आप इस कंटेनर को किसी भी डेटा प्रकार के साथ काम करने के लिए सामान्य करना चाहते हैं तो यदि आप उस 32-बिट को बुरा नहीं मानते हैं तो यह उपयोगी है। प्रति तत्व ओवरहेड।

क्या मुझे गतिशील आवंटन के लिए कोई मेमोरी पूल बनाना चाहिए, या क्या इससे परेशान होने की कोई जरूरत नहीं है? क्या होगा अगर लक्ष्य मंच मोबाइल डिवाइस हैं?

आवश्यकता एक मजबूत शब्द है और मैं बहुत प्रदर्शन-महत्वपूर्ण क्षेत्रों में काम कर रहा हूँ जैसे कि किरण, छवि प्रसंस्करण, कण सिमुलेशन और जाल प्रसंस्करण, लेकिन यह बहुत ही महंगा है और गोलियों की तरह बहुत हल्की प्रसंस्करण के लिए इस्तेमाल की जाने वाली नन्हा वस्तुओं को आवंटित करना बहुत महंगा है। और एक सामान्य प्रयोजन के लिए व्यक्तिगत रूप से कण, चर-आकार मेमोरी आवंटनकर्ता। यह देखते हुए कि आपको अपनी इच्छित किसी भी चीज़ को संग्रहीत करने के लिए एक या दो दिन में उपरोक्त डेटा संरचना को सामान्य करने में सक्षम होना चाहिए, मुझे लगता है कि इस तरह के ढेर आवंटन / सौदेबाजी की लागत को खत्म करने के लिए हर एक नन्हा किशोर चीज़ के लिए भुगतान किया जाना एक सार्थक विनिमय होगा। आवंटन / सौदे की लागत को कम करने के शीर्ष पर, आपको परिणामों का पता लगाने (कम कैश मिस और पेज दोष, यानी) का पता लगाने के संदर्भ का बेहतर इलाका मिलता है।

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

इसी तरह की नस में एक और रणनीति जो सी ++ में लागू करने के लिए थोड़ी आसान है, बस अपने मुख्य थ्रेड्स में अपनी वस्तुओं को मुक्त करने के लिए परेशान न करें। बस एक डेटा संरचना के अंत में जोड़ना और जोड़ना और जोड़ना जो बीच से चीजों को हटाने की अनुमति नहीं देता है। हालाँकि, उन चीजों को चिह्नित करें जिन्हें हटाने की आवश्यकता है। फिर एक अलग धागा हटाए गए तत्वों के बिना एक नई डेटा संरचना बनाने के महंगे काम का ख्याल रख सकता है और फिर पुराने एक के साथ नए को स्वैप कर सकता है, जैसे आवंटित करने और मुक्त करने वाले तत्वों की अधिकांश लागत एक पर पारित की जा सकती है। यदि आप यह धारणा बना सकते हैं कि किसी तत्व को हटाने का अनुरोध करने पर अलग थ्रेड को तत्काल संतुष्ट नहीं होना है। जहां तक ​​आपके धागों का संबंध है, न केवल मुक्त करना सस्ता बनाता है, बल्कि आवंटन को सस्ता बनाता है, चूंकि आप एक बहुत ही सरल और डम्बर डेटा संरचना का उपयोग कर सकते हैं, जिसे कभी भी बीच से हटाने के मामलों को संभालना नहीं पड़ता है। यह एक कंटेनर की तरह है जिसे केवल एक की जरूरत हैpush_backसम्मिलन के लिए फ़ंक्शन, clearसभी तत्वों को हटाने के लिए एक फ़ंक्शन, और swapहटाए गए तत्वों को छोड़कर एक नए, कॉम्पैक्ट कंटेनर के साथ सामग्री को स्वैप करने के लिए; यही वह है जहां तक ​​उत्परिवर्तन होता है।

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