.NET / C # टेल-कॉल पुनरावृत्ति के लिए ऑप्टिमाइज़ क्यों नहीं करता है?


111

मुझे यह सवाल मिला कि कौन सी भाषाएं पूंछ पुनरावृत्ति का अनुकूलन करती हैं। क्यों C # जब भी संभव हो, पूंछ पुनरावृत्ति का अनुकूलन नहीं करता है?

एक ठोस मामले के लिए, यह विधि लूप में क्यों अनुकूलित नहीं है ( विजुअल स्टूडियो 2008 32-बिट, यदि वह मायने रखता है) ?:

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}

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

5
2016 के बारे में जॉन स्कीट और स्कॉट हैनेलमैन द्वारा इसके बारे में उपयोगी बातचीत youtu.be/H2KkiRbDZyc?t=3302
Daniel B

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

जवाबों:


84

JIT संकलन संकलन चरण को करने में बहुत अधिक समय न बिताने के बीच एक मुश्किल संतुलनकारी कार्य है (इस प्रकार कम समय तक चलने वाले अनुप्रयोगों को काफी धीमा कर देता है) बनाम एक मानक आगे-आगे संकलन के साथ लंबे समय तक आवेदन को प्रतिस्पर्धी बनाए रखने के लिए पर्याप्त विश्लेषण नहीं करना। ।

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

CLR ही समर्थन पूंछ कॉल अनुकूलन करता है, लेकिन भाषा-विशिष्ट संकलक पता होना चाहिए कि प्रासंगिक उत्पन्न करने के लिए opcode और JIT यह सम्मान करने के लिए तैयार रहना चाहिए। एफ # का एफएससी प्रासंगिक ऑपकोड उत्पन्न करेगा (हालांकि एक साधारण पुनरावर्तन के लिए यह पूरी चीज को whileलूप में परिवर्तित कर सकता है )। C # का csc नहीं करता है।

देखें इस ब्लॉग पोस्ट (हाल ही में JIT परिवर्तन को देखते हुए अब पुराने हो चुके बहुत संभव है) कुछ जानकारी के लिए। ध्यान दें कि सीएलआर 4.0 x 86 के लिए बदलता है , x64 और ia64 इसका सम्मान करेंगे


2
इस पोस्ट को भी देखें: social.msdn.microsoft.com/Forums/en-US/netfxtoolsdev/thread/… जिसमें मुझे पता चलता है कि पूंछ एक नियमित कॉल की तुलना में धीमी है। खें!
प्लिंथ

77

यह Microsoft कनेक्ट फ़ीडबैक सबमिशन आपके प्रश्न का उत्तर देना चाहिए। इसमें Microsoft की आधिकारिक प्रतिक्रिया है, इसलिए मैं इसके द्वारा जाने की सलाह दूंगा।

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

सभी ने कहा, हम इसे देखना जारी रखते हैं, और हम भविष्य में संकलक की रिलीज में कुछ पैटर्न पा सकते हैं, जहां यह निर्देशों का अनुकरण करने के लिए बनाता है।

वैसे, के रूप में यह मुद्दा उठाया गया है, यह देखते हुए कि पूंछ प्रत्यावर्तन के लायक है है 64 पर अनुकूलित।


3
आपको यह उपयोगी भी लग सकता है: weblogs.asp.net/podwysocki/archive/2008/07/07/…
Noldorin

कोई संभावना नहीं, खुशी है कि आप इसे उपयोगी पाते हैं।
Noldorin

17
इसे उद्धृत करने के लिए धन्यवाद, क्योंकि यह अब 404 है!
रोमन स्टार्कोव

3
लिंक अब तय हो गया है।
लुकासन

15

C # टेल-कॉल रिकर्सन के लिए ऑप्टिमाइज़ नहीं करता है क्योंकि यही F # है!

सी # कंपाइलर को टेल-कॉल ऑप्टिमाइज़ेशन करने से रोकने वाली स्थितियों पर कुछ गहराई के लिए, इस लेख को देखें: JIT CLR टेल-कॉल की स्थिति

C # और F # के बीच अंतर

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

डेलिगेट इंटरऑपरेबिलिटी के लिए, इस लेख को देखें: एफ #, सी # और विजुअल बेसिक के बीच डेलिगेट इंटरऑपरेबिलिटी

C # और F # के बीच सैद्धांतिक और व्यावहारिक अंतर

यहां एक लेख है जो कुछ अंतरों को कवर करता है और सी # और एफ # के बीच पूंछ-कॉल पुनरावृत्ति के डिजाइन मतभेदों की व्याख्या करता है: सी # और एफ # में टेल-कॉल ओपोड उत्पन्न करना

यहाँ C #, F #, और C ++ \ CLI में कुछ उदाहरणों के साथ एक लेख दिया गया है: C #, F #, और C ++ \ CLI में टेल रिसर्शन में एडवेंचर्स।

मुख्य सैद्धांतिक अंतर यह है कि C # को लूप के साथ डिज़ाइन किया गया है जबकि F # को लैम्ब्डा कैलकुलस के सिद्धांतों पर डिज़ाइन किया गया है। लैंबडा कैलकुलस के सिद्धांतों पर एक बहुत अच्छी किताब के लिए , एबेल्सन, सुस्मान और सुस्मान द्वारा कंप्यूटर प्रोग्राम की संरचना और व्याख्या : इस मुफ्त पुस्तक को देखें ।

F # में टेल कॉल पर बहुत अच्छे परिचयात्मक लेख के लिए, इस लेख को देखें: F # में टेल कॉल का विस्तृत परिचय । अंत में, यहां एक लेख है जो गैर-पूंछ पुनरावृत्ति और पूंछ-कॉल पुनरावृत्ति (एफ # में) के बीच अंतर को कवर करता है: एफ-तीक्ष्ण बनाम गैर-पूंछ पुनरावृत्ति एफ तेज में


8

मुझे हाल ही में बताया गया था कि 64 बिट के लिए सी # संकलक पूंछ पुनरावृत्ति का अनुकूलन करता है।

C # इसे भी लागू करता है। हमेशा लागू नहीं होने का कारण यह है कि पूंछ पुनरावृत्ति को लागू करने के लिए उपयोग किए जाने वाले नियम बहुत सख्त हैं।


8
X64 घबराना यह करता है, लेकिन C # संकलक नहीं करता है
मार्क सोउल

जानकारी के लिए धन्यवाद। यह सफेद अलग है तो मैंने पहले क्या सोचा था।
अलेक्जेंड्रे ब्रिस्सोइस

3
बस इन दो टिप्पणियों को स्पष्ट करने के लिए, C # कभी CIL 'टेल' ओपेक का उत्सर्जन नहीं करता है, और मेरा मानना ​​है कि यह 2017 में अभी भी सही है। हालांकि, सभी भाषाओं के लिए, ओपकोड हमेशा इस अर्थ में सलाहकार है कि संबंधित जिटर (x86, x64) ) चुपचाप इसे अनदेखा कर देंगे यदि विविध परिस्थितियों को पूरा नहीं किया जाता है (ठीक है, संभावित स्टैक ओवरफ्लो को छोड़कर कोई त्रुटि नहीं है )। यह बताता है कि आप 'रिट' के साथ 'टेल' का अनुसरण करने के लिए मजबूर क्यों हैं - यह इस मामले के लिए है। इस बीच, जीआईटी भी अनुकूलन को लागू करने के लिए स्वतंत्र हैं जब CIL में कोई 'टेल' उपसर्ग नहीं है, फिर से उपयुक्त माना जाता है, और .NET भाषा की परवाह किए बिना।
ग्लेन स्लेडेन

3

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


Trampolines इनवेसिव हैं (वे कॉलिंग कन्वेंशन के लिए एक वैश्विक परिवर्तन हैं), उचित टेल कॉल उन्मूलन की तुलना में ~ 10x धीमा और वे सभी स्टैक ट्रेस जानकारी को डिफ्यूकेट करते हैं और इसे डिबग और प्रोफाइल कोड के लिए बहुत कठिन बनाते हैं
JE

1

जैसा कि अन्य उत्तर में कहा गया है, सीएलआर पूंछ कॉल अनुकूलन का समर्थन करता है और ऐसा लगता है कि यह ऐतिहासिक रूप से प्रगतिशील सुधारों के तहत था। लेकिन C # में इसका समर्थन करने Proposalसे C # प्रोग्रामिंग लैंग्वेज सपोर्ट टेल रिकर्सन # 2544 के डिजाइन के लिए git रिपॉजिटरी में एक ओपन इश्यू है ।

आप वहां कुछ उपयोगी विवरण और जानकारी पा सकते हैं। उदाहरण के लिए @jaykrell का उल्लेख किया गया है

मुझे जो पता है वह मुझे देने दो।

कभी-कभी टेलकॉल एक प्रदर्शन जीत-जीत है। यह CPU को बचा सकता है। jmp कॉल / रेट से सस्ता है यह स्टैक को बचा सकता है। कम स्टैक को छूना बेहतर इलाके के लिए बनाता है।

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

यदि कॉलर पैरामीटर कैलली पैरामीटर से अधिक बड़े होते हैं, तो यह आमतौर पर एक बहुत आसान जीत-जीत रूपांतरण होता है। पैरामीटर-स्थिति से लेकर पूर्णांक / फ्लोट तक, और सटीक StackMaps और इस तरह के उत्पन्न करने जैसे कारक हो सकते हैं।

अब, एक और कोण है, एल्गोरिदम का जो टेलक्लिम एलिमिनेशन की मांग करता है, नियत / छोटे स्टैक के साथ मनमाने ढंग से बड़े डेटा को प्रोसेस करने में सक्षम होने के प्रयोजनों के लिए। यह प्रदर्शन के बारे में नहीं है, लेकिन सभी को चलाने की क्षमता के बारे में है।

मुझे भी उल्लेख करना चाहिए (अतिरिक्त जानकारी के रूप में), जब हम System.Linq.Expressionsनेमस्पेस में अभिव्यक्ति वर्गों का उपयोग कर एक संकलित लैम्ब्डा उत्पन्न कर रहे हैं , तो 'tailCall' नाम का एक तर्क है, जैसा कि इसकी टिप्पणी में बताया गया है

एक बूल जो इंगित करता है कि निर्मित अभिव्यक्ति का संकलन करते समय पूंछ कॉल अनुकूलन लागू किया जाएगा।

मुझे अभी तक इसकी कोशिश नहीं की गई थी, और मुझे यकीन नहीं है कि यह आपके प्रश्न से संबंधित कैसे मदद कर सकता है, लेकिन संभवतः कोई इसे आज़मा सकता है और कुछ परिदृश्यों में उपयोगी हो सकता है:


var myFuncExpression = System.Linq.Expressions.Expression.Lambda<Func<  >>(body:  , tailCall: true, parameters:  );

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