मैं इन पहले से ही उत्कृष्ट उत्तरों के बीच यहां कूदना चाहता था और स्वीकार करता हूं कि मैंने वास्तव में पीछे की ओर काम कर रहे पॉलीमोर्फिक कोड को एंटी-पैटर्न के साथ switches
या if/else
शाखाओं में मापा लाभ के साथ बदसूरत दृष्टिकोण लिया है । लेकिन मैंने यह थोक नहीं किया, केवल सबसे महत्वपूर्ण रास्तों के लिए। यह इतना काला और सफेद होना जरूरी नहीं है।
डिस्क्लेमर के रूप में, मैं रेअरट्रिंग जैसे क्षेत्रों में काम करता हूं जहां शुद्धता को प्राप्त करना इतना मुश्किल नहीं है (और अक्सर फ़र्ज़ी और वैसे भी अनुमानित है) जबकि गति अक्सर सबसे अधिक प्रतिस्पर्धी गुणों में से एक है जो बाहर मांगी गई है। रेंडर समय में कमी अक्सर सबसे आम उपयोगकर्ता अनुरोधों में से एक है, हमारे साथ लगातार हमारे सिर को खरोंचने और यह पता लगाने के लिए कि इसे सबसे महत्वपूर्ण मापा पथों के लिए कैसे प्राप्त किया जाए।
सशर्त की बहुरूपता परावर्तन
सबसे पहले, यह समझने योग्य है कि सशर्त शाखाओं के बजाए बनाए रखने के पहलू ( switch
या if/else
बयानों का एक गुच्छा ) से बहुरूपता को प्राथमिकता क्यों दी जा सकती है । यहां मुख्य लाभ व्यापकता है ।
पॉलीमॉर्फ़िक कोड के साथ, हम अपने कोडबेस में एक नया उपप्रकार पेश कर सकते हैं, कुछ पॉलिमॉर्फ़िक डेटा संरचना में इसके उदाहरण जोड़ सकते हैं और सभी मौजूदा पॉलीमॉर्फिक कोड अभी भी बिना किसी संशोधन के स्वचालित रूप से काम करते हैं। यदि आपके पास एक बड़े कोडबेस में बिखरे हुए कोड का एक गुच्छा है, जो "यदि इस प्रकार का 'फू' है, के रूप से मिलता-जुलता है , तो आप अपने आप को कोड के 50 असमान वर्गों को अपडेट करने के लिए एक भयानक बोझ के साथ मिल सकते हैं। एक नई प्रकार की चीज, और अभी भी कुछ याद आ रही है।
बहुरूपता के रख-रखाव लाभ स्वाभाविक रूप से यहाँ कम हो जाते हैं यदि आपके पास सिर्फ एक जोड़े या आपके कोडबेस का एक खंड है, जिसे इस प्रकार के चेक करने की आवश्यकता है।
अनुकूलन बैरियर
मेरा सुझाव है कि ब्रांचिंग और पाइप लाइनिंग के दृष्टिकोण से इसे बहुत अधिक न देखें, और इसे अनुकूलन बाधाओं के संकलक डिजाइन मानसिकता से अधिक देखें। शाखा भविष्यवाणी में सुधार करने के तरीके हैं जो दोनों मामलों पर लागू होते हैं, जैसे उप-प्रकार पर आधारित डेटा को सॉर्ट करना (यदि यह एक क्रम में फिट बैठता है)।
इन दो रणनीतियों के बीच क्या अंतर अधिक होता है, यह आशावादी के पास अग्रिम में जानकारी की मात्रा है। एक फ़ंक्शन कॉल जिसे ज्ञात किया जाता है वह बहुत अधिक जानकारी प्रदान करता है, एक अप्रत्यक्ष फ़ंक्शन कॉल जो संकलन-समय पर एक अज्ञात फ़ंक्शन को कॉल करता है, एक ऑप्टिमाइज़ेशन बाधा की ओर जाता है।
जब फ़ंक्शन को ज्ञात किया जा रहा है, तो संकलक संरचना को अनियंत्रित कर सकता है और स्मितरेंस, इनलाइनिंग कॉल को स्क्वैश कर सकता है, संभावित एलियासिंग ओवरहेड को नष्ट कर सकता है, निर्देश / रजिस्टर आवंटन में बेहतर काम कर सकता है, संभवतः लूप और शाखाओं के अन्य रूपों को भी पुन: व्यवस्थित कर सकता है, हार्ड उत्पन्न करता है। जब उचित हो तो कोडित लघु LUTs (कुछ GCC 5.3 हाल ही में switch
एक कूद तालिका के बजाय परिणामों के लिए डेटा की हार्ड-कोडित LUT का उपयोग करके मुझे एक बयान से आश्चर्यचकित कर दिया )।
जब हम एक अप्रत्यक्ष फ़ंक्शन कॉल के मामले में मिश्रण में संकलन-समय के अज्ञात लोगों को पेश करना शुरू करते हैं, तो उनमें से कुछ लाभ खो जाते हैं, और यही वह स्थिति है जहां सशर्त शाखाएं एक किनारे की पेशकश कर सकती हैं।
मेमोरी ऑप्टिमाइजेशन
एक वीडियो गेम का उदाहरण लें जिसमें तंग पाश में बार-बार प्राणियों के अनुक्रम को संसाधित करना शामिल है। ऐसे मामले में, हमारे पास इस तरह के कुछ बहुरूपी कंटेनर हो सकते हैं:
vector<Creature*> creatures;
नोट: सादगी के लिए मैं unique_ptr
यहाँ से बचा ।
... जहां Creature
एक बहुरूपी आधार प्रकार है। इस मामले में, पॉलीमॉर्फिक कंटेनरों के साथ कठिनाइयों में से एक यह है कि वे अक्सर प्रत्येक उपप्रकार के लिए अलग-अलग / व्यक्तिगत रूप से मेमोरी आवंटित करना चाहते हैं (पूर्व: operator new
प्रत्येक व्यक्तिगत प्राणी के लिए डिफ़ॉल्ट फेंकने का उपयोग करके )।
यह अक्सर अनुकूलन के लिए पहली प्राथमिकता होगी (हमें इसकी आवश्यकता होनी चाहिए) मेमोरी-ब्रांच के बजाय। यहां एक रणनीति प्रत्येक उप-प्रकार के लिए एक निश्चित आवंटनकर्ता का उपयोग करना है, प्रत्येक उप-प्रकार के लिए बड़ी मात्रा में आवंटन और पूलिंग मेमोरी को आवंटित करके एक सन्निहित प्रतिनिधित्व को प्रोत्साहित करना है। इस तरह की रणनीति के साथ, यह निश्चित रूप से इस creatures
कंटेनर को उप-प्रकार (साथ ही पते) के आधार पर छाँटने में मदद कर सकता है , क्योंकि यह न केवल शाखा भविष्यवाणी में सुधार कर रहा है, बल्कि संदर्भ के स्थानीयता में भी सुधार कर रहा है (एक ही उपप्रकार के कई प्राणियों को एक्सेस करने की अनुमति देता है) बेदखली से पहले एक एकल कैश लाइन से)।
डेटा संरचनाओं और लूप्स का आंशिक विचलन
मान लीजिए कि आप इन सभी गतियों से गुजरे हैं और आप अभी भी अधिक गति की इच्छा रखते हैं। यह ध्यान देने योग्य है कि हमारे द्वारा यहां किया जाने वाला प्रत्येक चरण स्थिरता बनाए रखने में गिरावट है, और हम पहले से ही कुछ धातु-पीस चरण में कम प्रदर्शन वाले रिटर्न के साथ होंगे। इसलिए अगर हमें इस क्षेत्र में चलना है, तो हमें एक महत्वपूर्ण प्रदर्शन की मांग करने की आवश्यकता है, जहां हम छोटे और छोटे प्रदर्शन लाभ के लिए आगे भी स्थिरता बनाए रखने के लिए तैयार हैं।
फिर भी प्रयास करने के लिए अगला कदम (और हमेशा हमारे परिवर्तनों को वापस करने की इच्छा के साथ अगर यह बिल्कुल भी मदद नहीं करता है) मैन्युअल विचलन हो सकता है।
संस्करण नियंत्रण टिप: जब तक आप मुझसे अधिक अनुकूलन-प्रेमी नहीं हो जाते हैं, तब तक इस बिंदु पर एक नई शाखा बनाने की इच्छा हो सकती है, अगर हमारी अनुकूलन कोशिशें बहुत अच्छी तरह से घट सकती हैं। मेरे लिए यह सभी तरह के बिंदुओं के बाद भी ट्रायल और त्रुटि है, यहां तक कि हाथ में एक प्रोफाइलर के साथ भी।
फिर भी, हम इस मानसिकता थोक लागू नहीं है। हमारे उदाहरण को जारी रखते हुए, मान लीजिए कि इस वीडियो गेम में ज्यादातर मानव प्राणी शामिल हैं, अब तक। ऐसे मामले में, हम केवल उन्हें फहराकर और उनके लिए एक अलग डेटा संरचना बनाकर केवल मानव प्राणियों की भक्ति कर सकते हैं।
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
इसका तात्पर्य यह है कि हमारे कोडबेस के सभी क्षेत्रों में जो प्राणियों को संसाधित करने की आवश्यकता है, उन्हें मानव प्राणियों के लिए एक अलग विशेष-केस लूप की आवश्यकता है। फिर भी जो मनुष्यों के लिए गतिशील प्रेषण उपरि (या शायद, अधिक उचित रूप से, अनुकूलन बाधा) को समाप्त करता है, जो कि अब तक का सबसे आम प्राणी प्रकार है। यदि ये क्षेत्र संख्या में बड़े हैं और हम इसे वहन कर सकते हैं, तो हम यह कर सकते हैं:
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
vector<Creature*> creatures; // contains humans and other creatures
... अगर हम इसे बर्दाश्त कर सकते हैं, तो कम महत्वपूर्ण रास्ते रह सकते हैं जैसे वे हैं और बस सभी प्रकार के जीवों को अमूर्त रूप से संसाधित करते हैं। महत्वपूर्ण पथ humans
एक लूप में और other_creatures
दूसरे लूप में प्रक्रिया कर सकते हैं ।
हम इस रणनीति को आवश्यकतानुसार बढ़ा सकते हैं और संभावित रूप से कुछ लाभ प्राप्त कर सकते हैं, फिर भी यह ध्यान देने योग्य है कि हम प्रक्रिया में कितनी स्थिरता बनाए रख रहे हैं। यहां फ़ंक्शन टेम्प्लेट का उपयोग करके मैन्युअल रूप से तर्क की नकल किए बिना दोनों मनुष्यों और प्राणियों के लिए कोड उत्पन्न करने में मदद कर सकते हैं।
कक्षाओं का आंशिक विचलन
कुछ साल पहले मैंने ऐसा किया था जो वास्तव में सकल था, और मुझे यकीन भी नहीं है कि यह अब भी फायदेमंद है (यह सी ++ 03 युग में था), एक वर्ग का आंशिक रूप से विचलन था। उस मामले में, हम पहले से ही अन्य उद्देश्यों के लिए प्रत्येक उदाहरण के साथ एक क्लास आईडी जमा कर रहे थे (आधार वर्ग में एक गौण के माध्यम से पहुँचा जो गैर-आभासी था)। वहां हमने इसके अनुरूप कुछ किया (मेरी स्मृति थोड़ी धुंधली है):
switch (obj->type())
{
case id_common_type:
static_cast<CommonType*>(obj)->non_virtual_do_something();
break;
...
default:
obj->virtual_do_something();
break;
}
... जहां virtual_do_something
एक उपवर्ग में गैर-आभासी संस्करणों को कॉल करने के लिए लागू किया गया था। यह सकल है, मुझे पता है, एक फ़ंक्शन कॉल को समर्पित करने के लिए एक स्पष्ट स्थिर डाउनकास्ट कर रहा है। मुझे नहीं पता कि यह अब कितना फायदेमंद है क्योंकि मैंने वर्षों से इस प्रकार की कोशिश नहीं की है। डेटा-ओरिएंटेड डिज़ाइन के संपर्क में आने के बाद, मैंने डेटा स्ट्रक्चर्स और लूप्स को हॉट / कोल्ड फैशन में विभाजित करने की उपरोक्त रणनीति को कहीं अधिक उपयोगी माना, ऑप्टिमाइज़ेशन स्ट्रैटेजीज़ के लिए और अधिक दरवाजे खोलकर (और बहुत कम बदसूरत)।
थोक भक्ति
मुझे यह स्वीकार करना चाहिए कि मैंने अब तक अनुकूलन मानसिकता को लागू करने के बारे में कभी नहीं सोचा है, इसलिए मुझे लाभों का कोई पता नहीं है। मैंने उन मामलों में दूरदर्शिता में अप्रत्यक्ष कार्यों से परहेज किया है जहां मुझे पता था कि केवल सशर्त का एक केंद्रीय सेट होने वाला था (उदाहरण: केवल एक केंद्रीय स्थान प्रसंस्करण घटनाओं के साथ घटना प्रसंस्करण), लेकिन कभी भी एक बहुरूपी मानसिकता के साथ शुरू नहीं हुआ और सभी तरह से अनुकूलित किया गया यहाँ तक ऊपर।
सैद्धांतिक रूप से, यहां तात्कालिक लाभ वर्चुअल पॉइंटर की तुलना में एक प्रकार की पहचान करने का एक संभावित छोटा तरीका हो सकता है (उदा: एक एकल बाइट यदि आप इस विचार के लिए प्रतिबद्ध हो सकते हैं कि पूरी तरह से आपके अनुकूलन बाधाओं को दूर करने के अलावा 256 अद्वितीय प्रकार या कम हैं) ।
यह आसान-से-रखरखाव कोड लिखने के लिए कुछ मामलों में भी मदद कर सकता है (बनाम ऊपर दिए गए अनुकूलित मैन्युअल विचलन उदाहरण) यदि आप switch
अपने डेटा संरचनाओं और छोरों को उपप्रकार के आधार पर विभाजित किए बिना एक केंद्रीय कथन का उपयोग करते हैं, या यदि कोई आदेश है -इन मामलों में निर्भरता जहां चीजों को एक सटीक क्रम में संसाधित किया जाना है (भले ही वह हमें सभी जगह शाखा करने का कारण बनता है)। यह उन मामलों के लिए होगा जहां आपके पास बहुत से स्थान नहीं हैं जिन्हें करने की आवश्यकता है switch
।
जब तक यह यथोचित रूप से बनाए रखने के लिए आसान नहीं होता तब तक मैं आमतौर पर बहुत ही प्रदर्शन-महत्वपूर्ण मानसिकता के साथ भी इसकी सिफारिश नहीं करूंगा। "बनाए रखने में आसान" दो प्रमुख कारकों पर टिका होगा:
- वास्तविक एक्स्टेंसिबिलिटी की आवश्यकता नहीं है (एक्स: यह सुनिश्चित करने के लिए कि आपके पास प्रक्रिया करने के लिए बिल्कुल 8 प्रकार की चीजें हैं, और कभी भी नहीं )।
- आपके कोड में कई जगह नहीं हैं जिन्हें इन प्रकारों की जांच करने की आवश्यकता है (उदाहरण: एक केंद्रीय स्थान)।
... फिर भी मैं ज्यादातर मामलों में उपरोक्त परिदृश्य की सिफारिश करता हूं और आवश्यकतानुसार आंशिक विचलन द्वारा अधिक कुशल समाधान की ओर अग्रसर होता हूं। यह प्रदर्शन के साथ एक्स्टेंसिबिलिटी और मेंटेनेंस की जरूरतों को संतुलित करने के लिए आपको बहुत अधिक सांस लेने का कमरा देता है।
वर्चुअल फ़ंक्शंस बनाम फ़ंक्शन पॉइंटर्स
इस तरह की टॉप ऑफ करने के लिए, मैंने यहाँ देखा कि वर्चुअल फंक्शन बनाम फंक्शन पॉइंटर्स के बारे में कुछ चर्चा हुई थी। यह सच है कि आभासी कार्यों को कॉल करने के लिए थोड़े अतिरिक्त काम की आवश्यकता होती है, लेकिन इसका मतलब यह नहीं है कि वे धीमे हैं। प्रति-सहजता से, यह उन्हें और भी तेज कर सकता है।
यह यहाँ प्रति-सहज है, क्योंकि हम स्मृति पदानुक्रम की गतिशीलता पर ध्यान दिए बिना निर्देशों के संदर्भ में लागत को मापने के लिए उपयोग किए जाते हैं, जो बहुत अधिक महत्वपूर्ण प्रभाव डालते हैं।
यदि हम class
20 वर्चुअल फंक्शन्स के साथ तुलना कर रहे हैं, struct
जो 20 फंक्शन पॉइंट्स को स्टोर करता है, और दोनों को कई बार इंस्टेंट किया जाता है, तो class
इस मामले में प्रत्येक उदाहरण का मेमोरी ओवरहेड, 64-बिट मशीनों पर वर्चुअल पॉइंटर के लिए 8 बाइट्स, जबकि मेमोरी struct
160 बाइट्स का ओवरहेड है ।
व्यावहारिक लागत एक पूरी बहुत अधिक अनिवार्य और गैर-अनिवार्य कैश हो सकती है जो फ़ंक्शन बनाम तालिका के वर्ग के साथ वर्चुअल फ़ंक्शन (और संभवतः पर्याप्त इनपुट पैमाने पर पृष्ठ दोष) का उपयोग करके याद करती है। यह लागत एक आभासी तालिका अनुक्रमण के थोड़ा अतिरिक्त काम को बौना कर देती है।
मैंने लीगेसी सी कोडबेस (मैं उम्र से अधिक) के साथ निपटा दिया है, जहां इस तरह structs
के फ़ंक्शन पॉइंटर्स से भरा हुआ है, और कई बार तात्कालिक रूप से, वास्तव में महत्वपूर्ण प्रदर्शन लाभ (100% से अधिक सुधार) दिए हैं, उन्हें आभासी कार्यों के साथ कक्षाओं में बदलकर, और बस स्मृति उपयोग में भारी कमी, कैश-मित्रता में वृद्धि आदि के कारण।
दूसरी तरफ, जब तुलना सेब से सेब के बारे में अधिक हो जाती है, तो मैंने इसी प्रकार के परिदृश्यों में उपयोगी होने के लिए C ++ आभासी फ़ंक्शन मानसिकता से C-style फ़ंक्शन पॉइंटर मानसिकता में अनुवाद करने की विपरीत मानसिकता पाई है:
class Functionoid
{
public:
virtual ~Functionoid() {}
virtual void operator()() = 0;
};
... जहां वर्ग एक एकल औसत दर्जे का अतिव्यापी कार्य (या अगर हम वर्चुअल विध्वंसक की गिनती करते हैं) का भंडारण कर रहे थे। उन मामलों में, यह निश्चित रूप से महत्वपूर्ण पथों को इस में बदलने में मदद कर सकता है:
void (*func_ptr)(void* instance_data);
... आदर्श रूप से खतरनाक कलाकारों को / से छिपाने के लिए एक प्रकार-सुरक्षित इंटरफ़ेस के पीछे void*
।
उन मामलों में जहां हम एक एकल वर्चुअल फ़ंक्शन के साथ एक वर्ग का उपयोग करने के लिए लुभाते हैं, यह जल्दी से फ़ंक्शन पॉइंटर्स का उपयोग करने में मदद कर सकता है। एक बड़ा कारण यह भी जरूरी नहीं है कि फ़ंक्शन पॉइंटर को कॉल करने में कम लागत हो। यह इसलिए है क्योंकि हम ढेर के बिखरे हुए क्षेत्रों पर प्रत्येक अलग-अलग फंक्शनॉयड आवंटित करने के प्रलोभन का सामना नहीं करते हैं यदि हम उन्हें एक निरंतर संरचना में एकत्रित कर रहे हैं। इस तरह के दृष्टिकोण से ढेर-जुड़े और मेमोरी फ़्रेग्मेंटेशन ओवरहेड से बचना आसान हो सकता है यदि उदाहरण डेटा सजातीय हो, जैसे, और केवल व्यवहार भिन्न होता है।
इसलिए निश्चित रूप से कुछ ऐसे मामले हैं जहाँ फंक्शन पॉइंटर्स का उपयोग करने में मदद मिल सकती है, लेकिन अक्सर मैंने इसे दूसरे तरीके से पाया है यदि हम फंक्शन पॉइंटर्स के टेबल के एक समूह की तुलना किसी एकल में कर रहे हैं जिसके लिए केवल एक पॉइंटर की आवश्यकता होती है जिसे प्रति कक्षा उदाहरण में संग्रहीत किया जाना चाहिए । वह व्यवहार्य अक्सर एक या एक से अधिक L1 कैश लाइनों के साथ-साथ तंग छोरों में बैठे होंगे।
निष्कर्ष
तो वैसे भी, इस विषय पर मेरी छोटी स्पिन है। मैं इन क्षेत्रों में सावधानी बरतने की सलाह देता हूं। विश्वास माप, वृत्ति नहीं, और जिस तरह से ये अनुकूलन अक्सर स्थिरता बनाए रखते हैं, केवल उतना ही आगे बढ़ें जितना आप खर्च कर सकते हैं (और एक बुद्धिमान मार्ग स्थिरता के पक्ष में गलत होगा)।