सामान्य तौर पर, क्या ब्रांचिंग से बचने के लिए आभासी कार्यों का उपयोग करना उचित है?


21

लगता है कि एक शाखा की लागत की बराबरी करने के लिए निर्देशों का लगभग बराबर होना आभासी कार्यों का एक समान कार्य है:

  • निर्देश बनाम डेटा कैश मिस
  • अनुकूलन बाधा

यदि आप कुछ इस तरह देखते हैं:

if (x==1) {
   p->do1();
}
else if (x==2) {
   p->do2();
}
else if (x==3) {
   p->do3();
}
...

आपके पास एक सदस्य फ़ंक्शन सरणी हो सकती है, या यदि कई फ़ंक्शन समान श्रेणीकरण पर निर्भर करते हैं, या अधिक जटिल वर्गीकरण मौजूद है, तो वर्चुअल उपयोग करें:

p->do()

लेकिन, सामान्य तौर पर, वर्चुअल फ़ंक्शंस बनाम ब्रांचिंग कितने महंगे हैं। सामान्य करने के लिए पर्याप्त प्लेटफार्मों पर परीक्षण करना कठिन है, इसलिए मैं सोच रहा था कि क्या किसी के पास अंगूठे का एक नियम था (प्यारा अगर यह 4 के रूप में सरल था if, तो ब्रेकपॉइंट है)

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


12
वैसे, आपकी प्रदर्शन आवश्यकताएं क्या हैं? क्या आपके पास हार्ड नंबर हैं जिन्हें आपको हिट करना है, या आप समय से पहले अनुकूलन में संलग्न हैं? चीजों की भव्य योजना में ब्रांचिंग और आभासी दोनों तरीके बेहद सस्ते हैं (उदाहरण के लिए खराब एल्गोरिदम, I / O, या ढेर आवंटन की तुलना में)।
आमोन

4
जो कुछ भी भविष्य में परिवर्तन के रास्ते में प्राप्त करने के लिए अधिक पठनीय / लचीला / संभावना नहीं है, और एक बार आप यह काम कर रहा है तो रूपरेखा करें और देखें कि यह वास्तव में मायने रखती है है। आमतौर पर यह नहीं है।
Ixrec

1
प्रश्न: "लेकिन, सामान्य तौर पर, वर्चुअल फ़ंक्शन कितने महंगे हैं ..." उत्तर: अप्रत्यक्ष शाखा (विकिपीडिया)
21

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

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

जवाबों:


21

मैं इन पहले से ही उत्कृष्ट उत्तरों के बीच यहां कूदना चाहता था और स्वीकार करता हूं कि मैंने वास्तव में पीछे की ओर काम कर रहे पॉलीमोर्फिक कोड को एंटी-पैटर्न के साथ 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 प्रकार की चीजें हैं, और कभी भी नहीं )।
  • आपके कोड में कई जगह नहीं हैं जिन्हें इन प्रकारों की जांच करने की आवश्यकता है (उदाहरण: एक केंद्रीय स्थान)।

... फिर भी मैं ज्यादातर मामलों में उपरोक्त परिदृश्य की सिफारिश करता हूं और आवश्यकतानुसार आंशिक विचलन द्वारा अधिक कुशल समाधान की ओर अग्रसर होता हूं। यह प्रदर्शन के साथ एक्स्टेंसिबिलिटी और मेंटेनेंस की जरूरतों को संतुलित करने के लिए आपको बहुत अधिक सांस लेने का कमरा देता है।

वर्चुअल फ़ंक्शंस बनाम फ़ंक्शन पॉइंटर्स

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

यह यहाँ प्रति-सहज है, क्योंकि हम स्मृति पदानुक्रम की गतिशीलता पर ध्यान दिए बिना निर्देशों के संदर्भ में लागत को मापने के लिए उपयोग किए जाते हैं, जो बहुत अधिक महत्वपूर्ण प्रभाव डालते हैं।

यदि हम class20 वर्चुअल फंक्शन्स के साथ तुलना कर रहे हैं, structजो 20 फंक्शन पॉइंट्स को स्टोर करता है, और दोनों को कई बार इंस्टेंट किया जाता है, तो classइस मामले में प्रत्येक उदाहरण का मेमोरी ओवरहेड, 64-बिट मशीनों पर वर्चुअल पॉइंटर के लिए 8 बाइट्स, जबकि मेमोरी struct160 बाइट्स का ओवरहेड है ।

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

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

दूसरी तरफ, जब तुलना सेब से सेब के बारे में अधिक हो जाती है, तो मैंने इसी प्रकार के परिदृश्यों में उपयोगी होने के लिए C ++ आभासी फ़ंक्शन मानसिकता से C-style फ़ंक्शन पॉइंटर मानसिकता में अनुवाद करने की विपरीत मानसिकता पाई है:

class Functionoid
{
public:
    virtual ~Functionoid() {}
    virtual void operator()() = 0;
};

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

void (*func_ptr)(void* instance_data);

... आदर्श रूप से खतरनाक कलाकारों को / से छिपाने के लिए एक प्रकार-सुरक्षित इंटरफ़ेस के पीछे void*

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

इसलिए निश्चित रूप से कुछ ऐसे मामले हैं जहाँ फंक्शन पॉइंटर्स का उपयोग करने में मदद मिल सकती है, लेकिन अक्सर मैंने इसे दूसरे तरीके से पाया है यदि हम फंक्शन पॉइंटर्स के टेबल के एक समूह की तुलना किसी एकल में कर रहे हैं जिसके लिए केवल एक पॉइंटर की आवश्यकता होती है जिसे प्रति कक्षा उदाहरण में संग्रहीत किया जाना चाहिए । वह व्यवहार्य अक्सर एक या एक से अधिक L1 कैश लाइनों के साथ-साथ तंग छोरों में बैठे होंगे।

निष्कर्ष

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


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

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

माटेओ, यह स्पष्टीकरण एक तकनीकी नेतृत्व ने मुझे कई साल पहले दिया था। दी, यह सी ++ के लिए था, इसलिए वह कई विरासत के निहितार्थों पर विचार कर रहा होगा। कैसे vtables अनुकूलित कर रहे हैं की मेरी समझ को स्पष्ट करने के लिए धन्यवाद।
रॉबर्ट बैरन

अच्छे उत्तर के लिए धन्यवाद (+1)। मुझे आश्चर्य है कि यह कितना std के लिए लागू होता है :: आभासी कार्यों के बजाय यात्रा।
डेवफर

13

टिप्पणियों:

  • कई मामलों के साथ, वर्चुअल फ़ंक्शंस तेज़ होते हैं क्योंकि वाइबेट लुकअप एक O(1)ऑपरेशन है जबकि else if()सीढ़ी एक O(n)ऑपरेशन है। हालांकि, यह केवल तभी सही है जब मामलों का वितरण सपाट हो।

  • एकल के लिए if() ... else, सशर्त तेज़ है क्योंकि आप फ़ंक्शन कॉल को ओवरहेड से बचाते हैं।

  • इसलिए, जब आपके पास मामलों का एक फ्लैट वितरण होता है, तो एक ब्रेक-ईवन बिंदु मौजूद होना चाहिए। एकमात्र सवाल यह है कि यह कहाँ स्थित है।

  • यदि आप सीढ़ी या वर्चुअल फ़ंक्शन कॉल के switch()बजाय उपयोग else if()करते हैं, तो आपका कंपाइलर और भी बेहतर कोड उत्पन्न कर सकता है: यह एक शाखा को एक स्थान पर कर सकता है, जिसे टेबल से देखा जाता है, लेकिन जो फ़ंक्शन कॉल नहीं है। यही है, आपके पास सभी फ़ंक्शन कॉल ओवरहेड के बिना वर्चुअल फ़ंक्शन कॉल के सभी गुण हैं।

  • यदि कोई बाकी की तुलना में बहुत अधिक लगातार है, तो if() ... elseउस मामले के साथ शुरू करने से आपको सबसे अच्छा प्रदर्शन मिलेगा: आप एक एकल सशर्त शाखा को निष्पादित करेंगे जो कि अधिकांश मामलों में सही ढंग से भविष्यवाणी की गई है।

  • आपके संकलक को मामलों के अपेक्षित वितरण का कोई ज्ञान नहीं है और यह एक सपाट वितरण मान लेगा।

चूँकि आपके संकलक के पास कुछ अच्छे उत्तराधिकार हैं, switch()जैसे कि else if()सीढ़ी के रूप में या टेबल लुकअप के रूप में कब कोड करना है । जब तक आप यह नहीं जानेंगे कि मामलों का वितरण पक्षपातपूर्ण है, मैं इसके निर्णय पर विश्वास करना चाहूंगा।

तो, मेरी सलाह यह है:

  • यदि मामलों में से एक आवृत्ति के मामले में बाकी को बौना करता है, तो एक क्रमबद्ध else if()सीढ़ी का उपयोग करें ।

  • अन्यथा एक switch()बयान का उपयोग करें , जब तक कि अन्य तरीकों में से एक आपके कोड को अधिक पठनीय नहीं बनाता है। सुनिश्चित करें कि आप काफी कम पठनीयता के साथ एक नेगलेबल प्रदर्शन लाभ नहीं खरीदते हैं।

  • यदि आपने उपयोग किया है switch()और अभी भी प्रदर्शन से संतुष्ट नहीं हैं, तो तुलना करें, लेकिन यह पता लगाने के लिए तैयार रहें कि switch()पहले से ही सबसे तेज संभावना थी।


2
कुछ कंपाइलर एनोटेशन को कंपाइलर को यह बताने की अनुमति देते हैं कि कौन सा मामला सही होने की अधिक संभावना है, और वे कंपाइलर तेजी से कोड का उत्पादन कर सकते हैं जब तक एनोटेशन सही है।
gnasher729

5
O (n) या O (n ^ 20) की तुलना में O- (1) ऑपरेशन वास्तविक रूप से निष्पादन के समय में तेजी से आवश्यक नहीं है।
whatsisname

2
@whatsisname यही कारण है कि मैंने "कई मामलों के लिए" कहा। की परिभाषा से O(1)और O(n)वहाँ मौजूद है kताकि फ़ंक्शन सभी के लिए फ़ंक्शन O(n)से अधिक हो । एकमात्र सवाल यह है कि क्या आपके पास कई मामले होने की संभावना है। और, हां, मैंने कई मामलों के साथ बयानों को देखा है कि एक सीढ़ी निश्चित रूप से एक आभासी फ़ंक्शन कॉल या भरी हुई प्रेषण की तुलना में धीमी है। O(1)n >= kswitch()else if()
विस्फ़ोटक -

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

7

सामान्य तौर पर, क्या ब्रांचिंग से बचने के लिए आभासी कार्यों का उपयोग करना उचित है?

सामान्य तौर पर, हाँ। रखरखाव के लिए लाभ महत्वपूर्ण हैं (परीक्षण में अलगाव, चिंताओं को अलग करना, सुधार के प्रतिरूपकता और व्यापकता)।

लेकिन, सामान्य तौर पर, वर्चुअल फंक्शंस बनाम ब्रांचिंग कितने महंगे होते हैं, जिन्हें सामान्य बनाने के लिए पर्याप्त प्लेटफार्मों पर परीक्षण करना कठिन है, इसलिए मैं सोच रहा था कि क्या किसी के पास अंगूठे का एक नियम था (प्यारा अगर यह 4 के रूप में सरल था, तो ब्रेकपॉइंट है)

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

यही है, "वर्चुअल फ़ंक्शंस बनाम ब्रांचिंग कितना महंगा है" का सही जवाब है उपाय और पता करें।

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

आप कहते हैं कि आप चाहते हैं कि यह खंड यथासंभव तेज़ चले; कितना तेज है? आपकी ठोस आवश्यकता क्या है?

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

आभासी कार्यों का उपयोग करें। यह आपको आवश्यक होने पर प्रति प्लेटफ़ॉर्म को ऑप्टिमाइज़ करने की अनुमति देगा, और फिर भी क्लाइंट कोड को साफ रखेगा।


बहुत सारे रखरखाव की प्रोग्रामिंग करने के बाद, मैं थोड़ी सावधानी के साथ झंकार करने जा रहा हूं: रखरखाव के लिए वर्चुअल फ़ंक्शन IMNSHO के लिए बहुत खराब हैं, ठीक आपके द्वारा सूचीबद्ध फायदे के कारण। मुख्य समस्या उनका लचीलापन है; तुम वहाँ बहुत कुछ छड़ी कर सकते हैं ... और लोग करते हैं। यह गतिशील प्रेषण के बारे में सांख्यिकीय रूप से बहुत कठिन है। फिर भी अधिकांश विशिष्ट मामलों में कोड को सभी लचीलेपन की आवश्यकता नहीं होती है, और रनटाइम लचीलेपन को हटाने से कोड के बारे में तर्क करना आसान हो जाता है। फिर भी मैं इतनी दूर नहीं जाना चाहता कि यह कहना कि आपको गतिशील प्रेषण का उपयोग नहीं करना चाहिए; वह बेतुका है।
Eamon Nerbonne

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

5

अन्य उत्तर पहले से ही अच्छे सैद्धांतिक तर्क प्रदान करते हैं। मैं हाल ही में किए गए एक प्रयोग के परिणामों को जोड़ना चाहता हूं, यह अनुमान लगाने के लिए कि क्या switchऑप-कोड पर बड़े का उपयोग करके वर्चुअल मशीन (वीएम) को लागू करना एक अच्छा विचार होगा या ऑप-कोड की एक सूचकांक के रूप में व्याख्या करना होगा। फ़ंक्शन पॉइंटर्स की एक सरणी में। हालांकि यह एक virtualफ़ंक्शन कॉल के समान नहीं है , मुझे लगता है कि यह यथोचित करीब है।

मैंने 1 और 10000 के बीच बेतरतीब ढंग से उठाए गए (भले ही समान रूप से कम रेंज के अधिक रेंज का नमूना नहीं) के साथ वीएम के लिए एक पायथन स्क्रिप्ट को C ++ 14 कोड को बेतरतीब ढंग से उत्पन्न करने के लिए लिखा है। उत्पन्न वीएम में हमेशा 128 बहनें थीं और नहीं राम। निर्देश सार्थक नहीं हैं और सभी के निम्नलिखित रूप हैं।

inline void
op0004(machine_state& state) noexcept
{
  const auto c = word_t {0xcf2802e8d0baca1dUL};
  const auto r1 = state.registers[58];
  const auto r2 = state.registers[69];
  const auto r3 = ((r1 + c) | r2);
  state.registers[6] = r3;
}

स्क्रिप्ट भी एक switchबयान का उपयोग प्रेषण दिनचर्या उत्पन्न करता है ...

inline int
dispatch(machine_state& state, const opcode_t opcode) noexcept
{
  switch (opcode)
  {
  case 0x0000: op0000(state); return 0;
  case 0x0001: op0001(state); return 0;
  // ...
  case 0x247a: op247a(state); return 0;
  case 0x247b: op247b(state); return 0;
  default:
    return -1;  // invalid opcode
  }
}

... और समारोह संकेत की एक सरणी।

inline int
dispatch(machine_state& state, const opcode_t opcode) noexcept
{
  typedef void (* func_type)(machine_state&);
  static const func_type table[VM_NUM_INSTRUCTIONS] = {
    op0000,
    op0001,
    // ...
    op247a,
    op247b,
  };
  if (opcode >= VM_NUM_INSTRUCTIONS)
    return -1;  // invalid opcode
  table[opcode](state);
  return 0;
}

जो प्रेषण दिनचर्या उत्पन्न की गई थी, उसे प्रत्येक उत्पन्न वीएम के लिए यादृच्छिक रूप से चुना गया था।

बेंचमार्किंग के लिए, ऑप-कोड की धारा को रैंडमली सीडेड ( std::random_device) मेरसेन ट्विस्टर रैंडम इंजन ( std::mt19937_64) द्वारा उत्पन्न किया गया था ।

प्रत्येक वीएम के लिए कोड का उपयोग करके GCC 5.2.0 के साथ संकलित किया गया था -DNDEBUG, -O3और -std=c++14स्विच। सबसे पहले, यह -fprofile-generate1000 यादृच्छिक निर्देशों के अनुकरण के लिए एकत्र किए गए विकल्प और प्रोफाइल डेटा का उपयोग करके संकलित किया गया था । तब कोड को फिर से संकलित किया गया -fprofile-useथा ताकि एकत्र किए गए प्रोफाइल डेटा के आधार पर अनुकूलन की अनुमति हो।

वीएम को तब 50 000 000 चक्रों के लिए चार बार (एक ही प्रक्रिया में) व्यायाम किया गया था और प्रत्येक रन के लिए समय मापा गया था। कोल्ड-कैश प्रभाव को खत्म करने के लिए पहला रन छोड़ दिया गया था। PRNG को रनों के बीच फिर से बीजित नहीं किया गया था ताकि वे निर्देशों के समान क्रम का प्रदर्शन न करें।

इस सेटअप का उपयोग करते हुए, प्रत्येक प्रेषण दिनचर्या के लिए 1000 डेटा पॉइंट एकत्र किए गए थे। डेटा को क्वाड कोर AMD A8-6600K APU पर 2048 KiB कैश के साथ 64 बिट GNU / Linux पर बिना किसी ग्राफ़िकल डेस्कटॉप या अन्य प्रोग्राम के चलने पर एकत्र किया गया था। नीचे दिखाया गया है कि प्रत्येक वीएम के निर्देश के अनुसार औसत सीपीयू समय (मानक विचलन के साथ) का एक प्लॉट है।

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

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

बेंचमार्क के लिए सभी स्रोत कोड के साथ-साथ पूर्ण प्रयोगात्मक डेटा और एक उच्च रिज़ॉल्यूशन प्लॉट मेरी वेबसाइट पर पाया जा सकता है ।


3

Cmaster के अच्छे उत्तर के अलावा, जिसे मैंने उकेरा है, ध्यान रखें कि फ़ंक्शन पॉइंटर्स आमतौर पर वर्चुअल फ़ंक्शंस की तुलना में कड़ाई से तेज़ होते हैं। वर्चुअल फ़ंक्शंस आम तौर पर वस्तु से सूचक के लिए सबसे पहले एक सूचक का अनुसरण करता है, उचित रूप से अनुक्रमण करता है, और फिर एक फ़ंक्शन पॉइंटर को डीरेफर करता है। तो अंतिम चरण समान है, लेकिन शुरू में अतिरिक्त चरण हैं। इसके अलावा, वर्चुअल फ़ंक्शन हमेशा "इसे" एक तर्क के रूप में लेते हैं, फ़ंक्शन पॉइंटर्स अधिक लचीले होते हैं।

एक और बात ध्यान में रखें: यदि आपके महत्वपूर्ण मार्ग में एक लूप शामिल है, तो प्रेषण गंतव्य द्वारा लूप को छाँटने में मदद मिल सकती है। जाहिर है कि यह बकवास है, जबकि लूप का पता लगाना केवल n है, लेकिन अगर आप कई बार यात्रा करने जा रहे हैं तो यह इसके लायक हो सकता है। प्रेषण गंतव्य के आधार पर छाँटकर, आप यह सुनिश्चित करते हैं कि एक ही कोड को बार-बार निष्पादित किया जाता है, इसे icache में गर्म रखते हुए, कैश मिस को कम किया जाता है।

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

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


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

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

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

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

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

2

क्या मैं समझा सकता हूं कि मुझे लगता है कि यह एक XY- समस्या क्यों है ? (आप उन्हें पूछने में अकेले नहीं हैं।)

मुझे लगता है कि आपका वास्तविक लक्ष्य कुल मिलाकर समय बचाना है, न कि कैश-मिस और वर्चुअल फ़ंक्शंस के बारे में एक बिंदु को समझना।

यहां वास्तविक सॉफ्टवेयर में वास्तविक प्रदर्शन ट्यूनिंग का एक उदाहरण है ।

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

मैंने जिस उदाहरण से जोड़ा, उसमें मूल रूप से 2700 माइक्रोसेकंड प्रति "जॉब" लिया गया। पिज्जा के चारों ओर वामावर्त जा रहे हैं, छह समस्याओं की एक श्रृंखला तय की गई थी। पहले स्पीडअप ने 33% समय निकाला। दूसरे ने 11% निकाल दिए। लेकिन ध्यान दें, दूसरे को 11% समय पर नहीं मिला था, यह 16% था, क्योंकि पहली समस्या चली गई थी । इसी तरह, तीसरी समस्या को 7.4% से बढ़ाकर 13% (लगभग दोगुना) कर दिया गया क्योंकि पहले दो समस्याएं चली गई थीं।

अंत में, इस आवर्धन प्रक्रिया ने सभी लेकिन 3.7 माइक्रोसेकंड को समाप्त करने की अनुमति दी। यह मूल समय का 0.14% या 730x का स्पीडअप है।

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

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

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

क्या अंतिम कार्यक्रम इष्टतम था? शायद ऩही। किसी भी स्पीडअप का कैश मिस से कोई लेना देना नहीं था। क्या अब कैश की कमी महसूस होगी? शायद।

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


उन्होंने पहले ही कहा है कि उनके पास कई "अत्यधिक महत्वपूर्ण खंड" हैं जिनमें प्रदर्शन के हर अंतिम नैनोसेकंड की आवश्यकता होती है। तो यह उस सवाल का जवाब नहीं है जो उसने पूछा था (भले ही यह किसी और के सवाल का शानदार जवाब हो)
gbjbaanb

2
@ जीबीजैनब: अगर हर आखिरी नैनोसेकंड मायने रखता है, तो सवाल "सामान्य रूप से" क्यों शुरू होता है? यह बकवास है। जब नैनोसेकंड्स की गणना होती है, तो आप सामान्य उत्तरों की तलाश नहीं कर सकते, आप देखते हैं कि कंपाइलर क्या करता है, आप हार्डवेयर क्या करते हैं, आप बदलावों को देखते हैं, और आप हर भिन्नता को मापते हैं।
gnasher729

@ gnasher729 मुझे नहीं पता, लेकिन यह "अत्यधिक महत्वपूर्ण वर्गों" के साथ क्यों समाप्त होता है? मुझे लगता है, स्लैशडॉट की तरह, एक को हमेशा सामग्री पढ़नी चाहिए, न कि केवल शीर्षक!
gbjbaanb

2
@ जीबीजैनब: हर कोई कहता है कि उन्हें "अत्यधिक महत्वपूर्ण अनुभाग" मिल गए हैं। वे कैसे जानते हैं? मुझे नहीं पता कि कुछ महत्वपूर्ण है, जब तक मैं नहीं कहता, 10 नमूने लें, और इसे 2 या उससे अधिक पर देखें। इस तरह से एक मामले में, अगर तरीकों को 10 से अधिक निर्देशों को कहा जाता है, तो वर्चुअल फ़ंक्शन ओवरहेड संभवतः महत्वहीन है।
माइक डनलैवी

@ gnasher729: ठीक है, पहली बात मैं स्टैक नमूने प्राप्त करता हूं, और प्रत्येक पर, यह जांचें कि कार्यक्रम क्या कर रहा है और क्यों। फिर अगर यह अपना सारा समय कॉल ट्री की पत्तियों में बिताता है, और सभी कॉल हैं वास्तव में अपरिहार्य हैं , तो क्या इससे कोई फर्क पड़ता है कि कंपाइलर और हार्डवेयर क्या करते हैं। आप केवल विधि प्रेषण मामलों को जानते हैं यदि नमूने विधि प्रेषण करने की प्रक्रिया में उतरते हैं।
माइक डनलैवी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.