C ++ में वर्चुअल फ़ंक्शंस क्यों और कैसे धीमी होती हैं?


38

क्या कोई विस्तार से बता सकता है कि वर्चुअल फ़ंक्शंस को कॉल करने पर वर्चुअल टेबल कैसे काम करती है और किस पॉइंट से जुड़ी है।

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


5
एक व्यवहार्य से सही विधि कॉल को देखना स्पष्ट रूप से सीधे तरीके से कॉल करने की तुलना में अधिक समय लेने वाला है, क्योंकि ऐसा करने के लिए अधिक है। अपने स्वयं के कार्यक्रम के संदर्भ में अतिरिक्त समय कितना महत्वपूर्ण है या नहीं, यह एक और सवाल है। en.wikipedia.org/wiki/Virtual_method_table
रॉबर्ट हार्वे

10
वास्तव में क्या की तुलना में धीमी? मैंने कोड देखा है जिसमें बहुत सारे स्विच स्टेटमेंट के साथ गतिशील व्यवहार का टूटा हुआ, धीमा कार्यान्वयन था क्योंकि कुछ प्रोग्रामर ने सुना था कि वर्चुअल फ़ंक्शन धीमा हैं।
क्रिस्टोफर क्रुत्जिग

7
अक्सर बार, ऐसा नहीं है कि आभासी कॉल स्वयं धीमी होती है, लेकिन यह है कि संकलक के पास इनलाइन करने की क्षमता नहीं है।
केविन ह्सू

4
@ केविन ह्सु: हाँ यह बिल्कुल है। लगभग किसी भी समय कोई आपको बताता है कि उन्हें कुछ "वर्चुअल फ़ंक्शन कॉल ओवरहेड" को खत्म करने से गति मिली है, अगर आप इस पर गौर करते हैं कि वास्तव में सभी स्पीडअप कहां से आए हैं तो अनुकूलन से होगा जो अब संभव है क्योंकि कंपाइलर भर में अनुकूलन नहीं कर सकता है पहले से अनिश्चित कॉल।
समय

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

जवाबों:


55

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

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

बड़ा लेकिन : ये प्रदर्शन हिट आमतौर पर बात करने के लिए बहुत छोटे होते हैं। यदि आप एक उच्च-प्रदर्शन कोड बनाना चाहते हैं और एक वर्चुअल फ़ंक्शन जोड़ने पर विचार करना चाहते हैं जो कि खतरनाक आवृत्ति पर कहा जाएगा। हालांकि, यह भी ध्यान रखें कि शाखाओं के अन्य साधनों के साथ आभासी समारोह कॉल की जगह ( if .. else, switch, समारोह संकेत, आदि) मौलिक समस्या का समाधान नहीं होगा - यह बहुत अच्छी तरह से धीमी हो सकती है। समस्या (यदि यह सभी में मौजूद है) आभासी कार्य नहीं है लेकिन (अनावश्यक) अप्रत्यक्ष है।

संपादित करें: कॉल निर्देशों में अंतर अन्य उत्तरों में वर्णित है। मूल रूप से, एक स्थिर ("सामान्य") कॉल के लिए कोड है:

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

एक आभासी कॉल बिल्कुल एक ही काम करता है, सिवाय इसके कि फ़ंक्शन पता संकलन समय पर ज्ञात नहीं है। इसके बजाय, कुछ निर्देश ...

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

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


6
@ JörgWMittag वे सभी दुभाषिया सामान हैं, और वे अभी भी C ++ संकलक द्वारा उत्पन्न द्विआधारी कोड की तुलना में धीमी हैं
सैम

13
@ JörgWMittag ये अनुकूलन मुख्य रूप से अप्रत्यक्ष / देर से बाध्यकारी (लगभग) मुक्त करने के लिए मौजूद हैं जब इसकी आवश्यकता नहीं है , क्योंकि उन भाषाओं में हर कॉल तकनीकी रूप से देर से बाध्य है। यदि आप वास्तव में कम समय में एक ही स्थान से बहुत सारे आभासी तरीकों को कॉल करते हैं, तो ये अनुकूलन मदद नहीं करते हैं या सक्रिय रूप से चोट पहुँचाते हैं (शून्य के लिए बहुत सारे कोड बनाते हैं)। C ++ लोग उन ऑप्टिमाइज़ेशन में बहुत रुचि नहीं रखते हैं क्योंकि वे बहुत अलग स्थिति में हैं ...

10
@ JörgWMittag ... C ++ के लोग उन अनुकूलन में बहुत रुचि नहीं रखते हैं क्योंकि वे बहुत अलग स्थिति में हैं: AOT- संकलित व्यवहार्य तरीका पहले से ही बहुत तेज़ है, बहुत कम कॉल वास्तव में आभासी हैं, बहुरूपता के कई मामले जल्दी हैं- बाध्य (टेम्पलेट्स के माध्यम से) और इसलिए एओटी अनुकूलन के लिए संशोधन। अंत में, इन अनुकूलन को अनुकूल तरीके से करने के बजाय (केवल संकलन समय पर अटकलें लगाने के लिए) रन-टाइम कोड पीढ़ी की आवश्यकता होती है, जो टन के सिरदर्द का परिचय देती है । जेआईटी कंपाइलर उन समस्याओं को पहले ही अन्य कारणों से हल कर चुके हैं, इसलिए उन्हें कोई आपत्ति नहीं है, लेकिन एओटी कंपाइलर इससे बचना चाहते हैं।

3
शानदार जवाब, +1। एक बात पर ध्यान दें, हालांकि कभी-कभी ब्रांचिंग के परिणामों को संकलन के समय पर जाना जाता है, उदाहरण के लिए जब आप फ्रेमवर्क कक्षाएं लिखते हैं, जिन्हें विभिन्न उपयोगों का समर्थन करने की आवश्यकता होती है, लेकिन एक बार एप्लिकेशन कोड उन कक्षाओं के साथ इंटरैक्ट करता है जो विशिष्ट उपयोग पहले से ही ज्ञात हैं। इस स्थिति में, वर्चुअल फ़ंक्शंस का विकल्प, C ++ टेम्प्लेट हो सकता है। अच्छा उदाहरण CRTP होगा, जो किसी भी vtables के बिना आभासी फ़ंक्शन व्यवहार का अनुकरण करता है: en.wikipedia.org/wiki/Curiously_recurring_template_pattern
DXM

3
@ नाम आपके पास एक बिंदु है। मैंने जो कहने की कोशिश की वह यह है: किसी भी अप्रत्यक्ष की एक ही समस्या है, यह कुछ खास नहीं है virtual

23

यहां वर्चुअल फंक्शन कॉल और गैर-वर्चुअल कॉल से कुछ वास्तविक डिसबल्ड कोड क्रमशः दिए गए हैं:

mov    -0x8(%rbp),%rax
mov    (%rax),%rax
mov    (%rax),%rax
callq  *%rax

callq  0x4007aa

आप देख सकते हैं कि वर्चुअल कॉल को सही पता देखने के लिए तीन अतिरिक्त निर्देशों की आवश्यकता होती है, जबकि गैर-वर्चुअल कॉल के पते को इसमें संकलित किया जा सकता है।

हालाँकि, ध्यान दें कि ज्यादातर समय अतिरिक्त देखने का समय नगण्य माना जा सकता है। ऐसी स्थितियों में जहां लुकअप समय महत्वपूर्ण होगा, लूप की तरह, लूप से पहले पहले तीन निर्देशों को करके आमतौर पर वैल्यू को कैश किया जा सकता है।

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


1
समझने वाली बात यह है कि लगभग सभी मामलों में वाइबेट लुकअप और इनडायरेक्ट कॉल का इस्तेमाल विधि के कुल चल रहे समय पर नगण्य प्रभाव डालता है।
जॉन आर। स्ट्रोम

11
@ JohnR.Strohm एक आदमी की नगण्य चीज दूसरे आदमी की अड़चन है
James

1
-0x8(%rbp)। ओह माय ... वो एटी एंड टी सिंटैक्स।
अबेक्स

" तीन अतिरिक्त निर्देश " नहीं, केवल दो: vptr को लोड करना और फ़ंक्शन पॉइंटर को लोड करना
curiousguy

@ गंभीर यह वास्तव में तीन अतिरिक्त निर्देश हैं। आप भूल गए हैं कि एक वर्चुअल विधि को हमेशा एक पॉइंटर पर बुलाया जाता है , इसलिए आपको पहले पॉइंटर को एक रजिस्टर में लोड करना होगा। योग करने के लिए, पहला चरण उस पते को लोड करना है जो पॉइंटर चर रजिस्टर% rax में रखता है, फिर रजिस्टर में पते के अनुसार, इस पते पर vtpr को% rax रजिस्टर करने के लिए लोड करें, फिर इस पते के अनुसार रजिस्टर करें,% rax में कॉल की जाने वाली विधि का पता लोड करें, फिर कॉल *% rax!
गाब 19

18

क्या की तुलना में धीमी ?

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

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

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

दूसरे शब्दों में, आपका C ++ कंपाइलर तब काम कर सकता है जब वर्चुअल फ़ंक्शंस को वर्चुअल प्रेषण की आवश्यकता नहीं होती है, इसलिए आमतौर पर गैर-वर्चुअल फ़ंक्शंस के सापेक्ष उनके प्रदर्शन के बारे में चिंता करने का कोई कारण नहीं है।

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


4
"क्या की तुलना में धीमी?" - यदि आप एक विधि को आभासी बनाते हैं जो होना नहीं है, तो आपके पास बहुत अच्छी तुलनात्मक सामग्री है।
tdammers

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

12

क्योंकि एक आभासी कॉल के बराबर है

res_t (*foo)(arg_t);
foo = (obj->vtable[foo_offset]);
foo(obj,args)

जहां एक गैर-आभासी फ़ंक्शन के साथ कंपाइलर पहली पंक्ति को निरंतर-गुना कर सकता है, यह एक डीरफेरेंस है और एक डायनेमिक कॉल सिर्फ एक स्टैटिक कॉल में तब्दील हो जाता है

यह भी फ़ंक्शन को इनलाइन करने देता है (सभी उचित अनुकूलन परिणामों के साथ)

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