2011-05-28 10 views
11

मैं सी ++ और सी # दोनों का उपयोग करता हूं और मेरे दिमाग में कुछ ऐसा है कि इंटरफेस पर आभासी फ़ंक्शन कॉल को एलिड करने के लिए सी # में जेनिक्स का उपयोग करना संभव है या नहीं। निम्नलिखित पर विचार करें:क्या सी # जेनेरिक वर्चुअल फ़ंक्शन कॉल को विस्तारित करने के लिए उपयोग किया जा सकता है?

int Foo1(IList<int> list) 
{ 
    int sum = 0; 
    for(int i = 0; i < list.Count; ++i) 
     sum += list[i]; 
    return sum; 
} 

int Foo2<T>(T list) where T : IList<int> 
{ 
    int sum = 0; 
    for(int i = 0; i < list.Count; ++i) 
     sum += list[i]; 
    return sum; 
} 

/*...*/ 
var l = new List<int>(); 
Foo1(l); 
Foo2(l); 

अंदर Foo1, list.Count और सूची [i] के लिए हर पहुँच एक आभासी समारोह कॉल का कारण बनता है। यदि यह टेम्पलेट्स का उपयोग कर सी ++ थे, तो Foo2 पर कॉल में संकलक यह देख पाएगा कि वर्चुअल फ़ंक्शन कॉल को elided और इनलाइन किया जा सकता है क्योंकि कंक्रीट प्रकार टेम्पलेट तत्काल समय पर जाना जाता है।

लेकिन क्या यह सी # और जेनेरिक पर भी लागू होता है? जब आप Foo2 (l) को कॉल करते हैं, तो यह संकलन-समय पर ज्ञात है कि टी एक सूची है और इसलिए वह सूची। गणना और सूची [i] वर्चुअल फ़ंक्शन कॉल को शामिल करने की आवश्यकता नहीं है। सबसे पहले, क्या यह एक वैध अनुकूलन होगा जो कुछ तोड़ नहीं सकता है? और यदि हां, तो यह अनुकूलन बनाने के लिए पर्याप्त संकलक/जेआईटी स्मार्ट है?

उत्तर

8

यह एक दिलचस्प सवाल है, लेकिन दुर्भाग्य से, सिस्टम को "धोखा" के लिए आपका दृष्टिकोण आपके कार्यक्रम की दक्षता में सुधार नहीं करेगा। यदि यह हो सकता है, तो संकलक हमारे लिए सापेक्ष आसानी से कर सकता है!

आप सही हैं कि इंटरफ़ेस संदर्भ के माध्यम से IList<T> पर कॉल करते समय, विधियों को रनटाइम पर प्रेषित किया जाता है और इसलिए इनलाइन नहीं किया जा सकता है। इसलिए IList<T> पर कॉल Count जैसी विधियों और सूचकांक को इंटरफ़ेस के माध्यम से बुलाया जाएगा।

दूसरी तरफ, यह सच नहीं है कि आप किसी भी सामान्य लाभ के रूप में इसे फिर से लिखकर किसी भी प्रदर्शन लाभ (कम से कम वर्तमान सी # कंपाइलर और .NET4 सीएलआर के साथ नहीं) प्राप्त कर सकते हैं।

क्यों नहीं? पहले कुछ पृष्ठभूमि।सी # जेनेरिक काम यह है कि संकलक आपकी जेनेरिक विधि को संकलित करता है जिसमें प्रतिस्थापन योग्य पैरामीटर होते हैं और फिर वास्तविक पैरामीटर के साथ रन-टाइम पर उन्हें बदल देते हैं। यह आप पहले ही जानते थे।

लेकिन विधि का पैरामीटर संस्करण आपके से परिवर्तनीय प्रकारों के बारे में और नहीं जानता है और मैं संकलन समय पर करता हूं। इस मामले में, सभी संकलक Foo2 के बारे में जानते हैं कि list एक IList<int> है। हमारे पास जेनेरिक Foo2 में एक ही जानकारी है जो हम गैर-जेनेरिक Foo1 में करते हैं।

वास्तव में, कोड-ब्लोट से बचने के लिए, जेआईटी कंपाइलर केवल सभी संदर्भ प्रकार के लिए जेनेरिक विधि का एक ही तत्कालता उत्पन्न करता है। यहाँ Microsoft documentation कि इस प्रतिस्थापन और इन्स्टेन्शियशन का वर्णन करता है:

ग्राहक, एक संदर्भ प्रकार निर्दिष्ट करता तो JIT कम्पाइलर वस्तु के साथ सर्वर आईएल में सामान्य मापदंडों को बदल देता है, और मूल कोड में यह संकलित करता है। उस कोड का उपयोग सामान्य प्रकार पैरामीटर के बजाय संदर्भ प्रकार के लिए किसी और अनुरोध में किया जाएगा। ध्यान दें कि इस तरह जेआईटी कंपाइलर केवल वास्तविक कोड का उपयोग करता है। प्रबंधित ढेर से उनके आकार के अनुसार अभी भी आवंटित किए गए हैं, और कोई कास्टिंग नहीं है।

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

संपादित करें: अंत में, अनुभव, मैं सिर्फ दोनों Foo1 और Foo2 के बेंचमार्क किया है और वे समान प्रदर्शन परिणाम। दूसरे शब्दों में, Foo2Foo1 से कोई भी तेज़ नहीं है।

int Foo0(List<int> list) 
{ 
    int sum = 0; 
    for (int i = 0; i < list.Count; ++i) 
     sum += list[i]; 
    return sum; 
} 

यहाँ प्रदर्शन की तुलना है:

की तुलना के लिए एक "inlinable" संस्करण Foo0 जोड़ें

Foo0 = 1719 
Foo1 = 7299 
Foo2 = 7472 
Foo0 = 1671 
Foo1 = 7470 
Foo2 = 7756 

तो आप देख सकते है कि Foo0, जो inlined जा सकता है, नाटकीय रूप से तेजी से होता है अन्य दो की तुलना में। आप यह भी देख सकते हैं कि Foo2Foo0 जितनी जल्दी हो सके कहीं भी धीमा है।

+0

बहुत अच्छा। LINQPad में 'Foo1' और' Foo0' के लिए आईएल को देखते हुए मतभेद अच्छी तरह से दिखाते हैं। 'Foo1' में' callvirt' ऑप्स 'IList के लिए कॉल दिखाने get_Item' और' ICollection .get_Count' जबकि 'Foo0' के' callvirt' ऑप्स हैं 'सूची .get_Item' और 'सूची .get_Count'। 'Ldarga.s' को निष्पादित करने की आवश्यकता से' Foo2' की धीमाता को समझाया जा सकता है और फिर 'ldarg.1' निष्पादित करने के बजाय 'curvirted' के साथ 'callvirt' को उपसर्ग कर सकता है। – arcain

+1

क्या इसका मतलब यह नहीं है कि आप अपने इंटरफेस को लागू करने वाले structs बनाकर सिस्टम को धोखा दे सकते हैं, फिर अपने जेनेरिक विधि/कक्षा में उन structs का उपयोग करें? चूंकि वे मूल्य प्रकार हैं, रनटाइम प्रत्येक संरचना प्रकार के लिए कई ठोस कार्यान्वयन बनाएगा ... – Cesar

4

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

.net जेनरिक के बारे में याद करने के लिए बात यह है कि दी गई है:

Foo<T>; 

तो

Foo<Int32> 

रनटाइम पर एक मान्य प्रकार, अलग और

Foo<String> 

से अलग है, और सभी वर्चुअल और गैर वर्चुअल विधियों के अनुसार इलाज किया जाता है। यही वजह है कि आप एक

List<Vehicle> 

बनाने और इसे करने के लिए एक कार में जोड़ सकते हैं, लेकिन आप प्रकार का एक चर

List<Vehicle> 

नहीं बना सकते हैं और

का एक उदाहरण के लिए अपने मूल्य निर्धारित कर सकते हैं
List<Car> 

। वे विभिन्न प्रकार के हैं, लेकिन पूर्व में Add(...) विधि है जो Vehicle का तर्क लेती है, Car का एक सुपरटेप।

+1

मैं एक आभासी समारोह कॉल किस हद तक बच सकते हैं करने के लिए यकीन नहीं है, लेकिन ऐसे मामलों में जहां एक अंतरफलक एक struct द्वारा लागू किया जा सकता है, एक एक सामान्य पैरामीटर जो बजाय एक इंटरफेस करने के लिए विवश किया जाता है के रूप में struct पास करके मुक्केबाजी समाप्त कर सकते हैं एक इंटरफेस के रूप में इसे पारित करने के लिए। – supercat

संबंधित मुद्दे

 संबंधित मुद्दे