क्या लूपिंग की तुलना में पुनरावृत्ति कभी तेज होती है?


286

मुझे पता है कि पुनरावृत्ति कभी-कभी लूपिंग की तुलना में बहुत अधिक क्लीनर होती है, और मैं कुछ भी नहीं पूछ रहा हूं कि मुझे पुनरावृत्ति पर पुनरावृत्ति का उपयोग कब करना चाहिए, मुझे पता है कि पहले से ही इसके बारे में बहुत सारे सवाल हैं।

क्या मैं पूछ रहा हूँ, प्रत्यावर्तन है कभी एक पाश की तुलना में तेजी? मेरे लिए ऐसा लगता है, आप हमेशा एक लूप को निखारने में सक्षम होंगे और इसे एक पुनरावर्ती फ़ंक्शन की तुलना में अधिक तेज़ी से प्रदर्शन करने के लिए प्राप्त कर सकते हैं क्योंकि लूप अनुपस्थित है लगातार नए स्टैक फ्रेम स्थापित करना।

मैं विशेष रूप से यह देख रहा हूं कि उन अनुप्रयोगों में पुनरावृत्ति तेजी से होती है जहां डेटा को संभालने का सही तरीका है, जैसे कि कुछ छंटाई कार्यों में, बाइनरी पेड़ों में, आदि।


3
कभी-कभी कुछ पुनरावृत्ति के लिए पुनरावृत्ति प्रक्रिया या बंद-फार्म के फार्मूले को चालू होने में सदियों लगते हैं। मुझे लगता है कि केवल उन समयों पर पुनरावृत्ति तेज होती है :) lol
प्रतीक देवघर

24
खुद के लिए बोलते हुए, मैं इसे पसंद करते हैं। ;-)
इटरेटर



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

जवाबों:


357

यह इस्तेमाल की जा रही भाषा पर निर्भर करता है। आपने 'भाषा-अज्ञेयवादी' लिखा है, इसलिए मैं कुछ उदाहरण दूंगा।

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

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

मुझे पता है कि कुछ योजनाओं के क्रियान्वयन में, आम तौर पर पुनरावृत्ति लूपिंग से तेज होगी।

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

परिशिष्ट: कुछ वातावरणों में, सबसे अच्छा विकल्प न तो पुनरावृत्ति है और न ही पुनरावृत्ति है, बल्कि उच्च क्रम वाले कार्य हैं। इनमें "मैप", "फिल्टर" और "कम" (जिसे "फोल्ड" भी कहा जाता है) शामिल हैं। न केवल ये पसंदीदा शैली हैं, न केवल वे अक्सर क्लीनर हैं, लेकिन कुछ वातावरणों में ये कार्य स्वत: समानांतर से बढ़ावा पाने के लिए पहले (या केवल) हैं - इसलिए वे पुनरावृत्ति या पुनरावृत्ति की तुलना में काफी तेज हो सकते हैं। डेटा समानांतर हास्केल ऐसे वातावरण का एक उदाहरण है।

सूची की समझ एक और विकल्प है, लेकिन ये आमतौर पर पुनरावृत्ति, पुनरावृत्ति, या उच्चतर आदेश कार्यों के लिए केवल शक्करयुक्त चीनी हैं।


48
मैं +1 करता हूं, और यह टिप्पणी करना चाहता हूं कि "पुनरावृत्ति" और "लूप्स" केवल वही हैं जो मनुष्य अपने कोड का नाम देते हैं। प्रदर्शन के लिए जो मायने रखता है वह यह नहीं है कि आप चीजों को कैसे नाम देते हैं, बल्कि यह भी कि वे कैसे संकलित / व्याख्यायित होते हैं। पुनरावृत्ति, परिभाषा के अनुसार, एक गणितीय अवधारणा है, और स्टैक फ्रेम और विधानसभा सामान के साथ बहुत कम है।
पी शेव्ड

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

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

जो सबसे ज्यादा मायने रखता है वह है ऑपरेशन नहीं। जितना अधिक आप "आईओ", उतना ही आपको प्रक्रिया करना होगा। Un-IOing डेटा (उर्फ अनुक्रमण) हमेशा किसी भी सिस्टम के लिए सबसे बड़ा प्रदर्शन को बढ़ावा देने वाला होता है क्योंकि आपको इसे पहली जगह में संसाधित नहीं करना पड़ता है।
जेफ फिशर

53

क्या लूप की तुलना में रिकर्सन कभी तेज होता है?

नहीं, पुनरावृत्ति की तुलना में Iteration हमेशा तेज़ रहेगा। (एक वॉन न्यूमैन आर्किटेक्चर में)

स्पष्टीकरण:

यदि आप एक सामान्य कंप्यूटर के न्यूनतम संचालन को खरोंच से बनाते हैं, तो "इटरेशन" पहले बिल्डिंग ब्लॉक के रूप में आता है और "रिकर्सन" की तुलना में कम संसाधन गहन होता है, एर्गो तेजी से होता है।

खरोंच से एक छद्म कंप्यूटिंग मशीन का निर्माण:

अपने आप से सवाल करें : एक एल्गोरिथ्म का पालन करने और परिणाम तक पहुंचने के लिए आपको एक मूल्य की गणना करने की क्या आवश्यकता है ?

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

  1. पहला कॉन्सेप्ट: मेमोरी सेल्स, स्टोरेज, स्टेट । कुछ करने के लिए आपको अंतिम और मध्यवर्ती परिणाम मूल्यों को संग्रहीत करने के लिए स्थानों की आवश्यकता होती है। मान लेते हैं कि हमारे पास "पूर्णांक" कोशिकाओं की एक अनंत सरणी है, जिसे मेमोरी कहा जाता है , एम [0..Infinite]।

  2. निर्देश: कुछ करें - सेल को रूपांतरित करें, उसका मान बदलें। परिवर्तन की अवस्था । प्रत्येक दिलचस्प निर्देश एक परिवर्तन करता है। मूल निर्देश हैं:

    a) मेमोरी सेल्स सेट और मूव करें

    • एक मान को मेमोरी में स्टोर करें, जैसे: स्टोर 5 मीटर [4]
    • मूल्य को किसी अन्य स्थिति में कॉपी करें: उदाहरण के लिए: स्टोर m [4] m [8]

    b) तर्क और अंकगणित

    • और, या, एक्सोर, नहीं
    • जोड़ें, उप, mul, div। उदाहरण के लिए m [7] m [8] जोड़ें
  3. एक निष्पादन एजेंट : एक आधुनिक सीपीयू में एक कोर । एक "एजेंट" कुछ ऐसा है जो निर्देशों को निष्पादित कर सकता है। एक एजेंट कागज पर एल्गोरिथ्म का पालन करने वाला व्यक्ति भी हो सकता है।

  4. चरणों का क्रम : निर्देशों का एक क्रम : यानी: यह पहले करो, इसके बाद करो आदि। निर्देशों का अनिवार्य अनुक्रम। यहां तक ​​कि एक पंक्ति के भाव "निर्देशों का अनिवार्य अनुक्रम" हैं। यदि आपके पास एक विशिष्ट "मूल्यांकन के आदेश" के साथ एक अभिव्यक्ति है, तो आपके पास कदम हैं । इसका मतलब यह है कि एक एकल रचित अभिव्यक्ति में "कदम" निहित है और एक अंतर्निहित स्थानीय चर भी है (इसे "परिणाम" कहते हैं)। उदाहरण के लिए:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    उपरोक्त अभिव्यक्ति का तात्पर्य 3 चरणों में निहित "परिणाम" चर से है।

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

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

  5. निर्देश सूचक : यदि आपके पास चरणों का अनुक्रम है, तो आपके पास एक अंतर्निहित "निर्देश सूचक" भी है। निर्देश सूचक अगला निर्देश चिह्नित करता है, और निर्देश पढ़ने के बाद अग्रिम होता है, लेकिन निर्देश निष्पादित होने से पहले।

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

  6. कूदो - एक बार जब आपके पास चरणों की संख्या और एक निर्देश सूचक होता है , तो आप निर्देश सूचक के मूल्य को बदलने के लिए " स्टोर " निर्देश को लागू कर सकते हैं । हम स्टोर निर्देश के इस विशिष्ट उपयोग को एक नए नाम के साथ कहेंगे : कूदो । हम एक नए नाम का उपयोग करते हैं क्योंकि एक नई अवधारणा के रूप में इसके बारे में सोचना आसान है। इंस्ट्रक्टर पॉइंटर को बदलकर हम एजेंट को "स्टेप x पर जाने" का निर्देश दे रहे हैं।

  7. अनंत उत्थान : वापस कूदकर, अब आप एजेंट को एक निश्चित संख्या में "रिपीट" कर सकते हैं। इस बिंदु पर हमारे पास अनंत उत्तेजना है।

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. सशर्त - निर्देशों का सशर्त निष्पादन। "सशर्त" खंड के साथ, आप वर्तमान स्थिति के आधार पर कई निर्देशों में से एक को निष्पादित कर सकते हैं (जो पिछले निर्देश के साथ सेट किया जा सकता है)।

  9. उचित Iteration : अब सशर्त खंड के साथ, हम जंप बैक अनुदेश के अनंत लूप से बच सकते हैं । अब हमारे पास सशर्त लूप है और फिर उचित Iteration है

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. नामकरण : किसी विशिष्ट मेमोरी लोकेशन को डेटा देने या स्टेप रखने के लिए नाम देना । यह सिर्फ एक "सुविधा" है। हम स्मृति स्थानों के लिए "नाम" को परिभाषित करने की क्षमता होने से कोई नया निर्देश नहीं जोड़ते हैं। "नामकरण" एजेंट के लिए एक निर्देश नहीं है, यह हमारे लिए सिर्फ एक सुविधा है। नामकरण कोड बनाता है (इस बिंदु पर) पढ़ने में आसान और बदलने में आसान।

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. एक-स्तरीय सबरूटीन : मान लीजिए कि आपके द्वारा अक्सर निष्पादित किए जाने वाले चरणों की एक श्रृंखला है। आप स्मृति में एक नामित स्थिति में चरणों को संग्रहीत कर सकते हैं और फिर उस स्थिति में कूद सकते हैं जब आपको उन्हें (कॉल) निष्पादित करने की आवश्यकता होती है। अनुक्रम के अंत में आपको निष्पादन जारी रखने के लिए कॉलिंग के बिंदु पर वापस लौटना होगा । इस तंत्र के साथ, आप मुख्य निर्देशों की रचना करके नए निर्देश (सबरूटीन्स) बना रहे हैं ।

    कार्यान्वयन: (कोई नई अवधारणाओं की आवश्यकता नहीं)

    • पूर्वनिर्धारित स्मृति स्थिति में वर्तमान निर्देश सूचक को संग्रहीत करें
    • कूद सबरूटीन के लिए
    • सबरूटीन के अंत में, आप पूर्वनिर्धारित मेमोरी लोकेशन से इंस्ट्रक्शन पॉइंटर को पुनः प्राप्त करते हैं, प्रभावी रूप से मूल कॉल के निम्न निर्देश पर वापस कूदते हैं।

    एक-स्तरीय कार्यान्वयन के साथ समस्या : आप एक सबरूटीन से दूसरे सबरूटीन को कॉल नहीं कर सकते। यदि आप करते हैं, तो आप रिटर्निंग एड्रेस (ग्लोबल वैरिएबल) को अधिलेखित कर देंगे, ताकि आप कॉल न कर सकें।

    एक करवाने के लिए सबरूटीन्स के लिए बेहतर कार्यान्वयन: आप एक ढेर की जरूरत है

  12. स्टैक : आप एक मेमोरी स्टैक को "स्टैक" के रूप में कार्य करने के लिए परिभाषित करते हैं, आप स्टैक पर मानों को "पुश" कर सकते हैं, और अंतिम "पुश" मान को "पॉप" भी कर सकते हैं। स्टैक को लागू करने के लिए आपको स्टैक पॉइंटर (इंस्ट्रक्शन पॉइंटर के समान) की आवश्यकता होगी जो स्टैक के वास्तविक "हेड" की ओर इशारा करता है। जब आप किसी मान को "पुश" करते हैं, तो स्टैक पॉइंटर घटता है और आप मूल्य को स्टोर करते हैं। जब आप "पॉप" करते हैं, तो आपको वास्तविक स्टैक पॉइंटर पर मूल्य मिलता है और फिर स्टैक पॉइंटर को बढ़ाया जाता है।

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

  14. पुनरावर्तन : क्या होता है जब एक सबरूटीन खुद को बुलाता है ?। इसे "पुनरावृत्ति" कहा जाता है।

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

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

...

पुनरावर्तन तक पहुँचने के बाद हम यहाँ रुक जाते हैं।

निष्कर्ष:

एक वॉन न्यूमैन आर्किटेक्चर में, स्पष्ट रूप से "पुनरावृत्ति" की तुलना में एक सरल / मूल अवधारणा है "Recursion" । हम का एक रूप है "पुनरावृत्ति" है, जबकि स्तर 7 पर "Recursion" अवधारणाओं पदानुक्रम के 14 के स्तर पर है।

मशीन कोड में Iteration हमेशा तेज़ होगा क्योंकि इसका अर्थ है कि कम निर्देश इसलिए कम CPU चक्र।

इनमे से कौन बेहतर है"?

  • जब आप सरल, अनुक्रमिक डेटा संरचनाओं को संसाधित कर रहे हैं, तो आपको "पुनरावृत्ति" का उपयोग करना चाहिए, और हर जगह एक "सरल लूप" करेगा।

  • जब आपको पुनरावर्ती डेटा संरचना (मुझे उन्हें "फ्रैक्टल डेटा स्ट्रक्चर्स" कहना पसंद है), या जब पुनरावर्ती समाधान स्पष्ट रूप से अधिक "सुरुचिपूर्ण" है, तो आपको "पुनरावर्तन" का उपयोग करना चाहिए।

सलाह : नौकरी के लिए सबसे अच्छा उपकरण का उपयोग करें, लेकिन बुद्धिमानी से चुनने के लिए प्रत्येक उपकरण के आंतरिक कामकाज को समझें।

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


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

2
"पुनरावृत्ति को छलांग में बदल दिया जा सकता है" झूठा है। सचमुच उपयोगी पुनरावर्तन को एक छलांग में नहीं बदला जा सकता है। टेल कॉल "पुनरावर्तन" एक विशेष मामला है, जहां आप "पुनरावृत्ति" के रूप में कोड करते हैं, जिसे कंपाइलर द्वारा लूप में सरल बनाया जा सकता है। इसके अलावा, आप "पुनरावृत्ति" के साथ "अपरिवर्तनीय" को स्वीकार कर रहे हैं, वे ऑर्थोगोनल अवधारणाएं हैं।
लुसियो एम। टेटो

"वास्तव में उपयोगी पुनरावर्तन को छलांग में नहीं बदला जा सकता" -> ताकि टेल कॉल ऑप्टिमाइज़ेशन किसी तरह बेकार हो? इसके अलावा, अपरिवर्तनीय और पुनरावृत्ति ऑर्थोगोनल हो सकती है, लेकिन आप उत्परिवर्ती काउंटरों के साथ लिंक लूपिंग करते हैं - अपने चरण 9 को देखें। मुझे लगता है कि आप सोच रहे हैं कि लूपिंग और पुनरावृत्ति मौलिक अलग अवधारणाएं हैं; वे नहीं हैं stackoverflow.com/questions/2651112/…
hmijail mourns resignees

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

34

रिकर्सियन तेजी से हो सकता है जहां विकल्प स्पष्ट रूप से स्टैक का प्रबंधन करना है, जैसे आपके द्वारा छांटे गए बाइनरी ट्री एल्गोरिदम में।

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

इसलिए सही तरीका यह है कि सबसे पहले इसे सबसे प्राकृतिक तरीके से लिखें, केवल यह चुनें कि यदि प्रोफाइल दिखाता है कि यह महत्वपूर्ण है, और तब अपेक्षित सुधार को मापें।


2
+1 के लिए " पहले इसे सबसे प्राकृतिक तरीके से लिखें " और विशेष रूप से " केवल अनुकूलन करें अगर प्रोफाइल दिखाता है कि यह महत्वपूर्ण है "
ट्रिपहाउंड

2
+1 स्वीकार करने के लिए कि हार्डवेयर स्टैक किसी सॉफ़्टवेयर की तुलना में तेज़ हो सकता है, मैन्युअल रूप से कार्यान्वित किया जाता है, इन-हीप स्टैक। प्रभावी रूप से दिखा रहा है कि सभी "नहीं" उत्तर गलत हैं।
sh1

12

टेल रीसर्शन लूपिंग की तरह तेज है। कई कार्यात्मक भाषाओं में पूंछ पुनरावृत्ति को लागू किया गया है।


35
पूंछ पुनरावृत्ति लूपिंग के रूप में तेज़ हो सकती है , जब एक पूंछ कॉल अनुकूलन लागू किया जाता है: c2.com/cgi/wiki?TailCallOptimization
Joachim Sauer

12

विचार करें कि प्रत्येक के लिए क्या बिल्कुल किया जाना चाहिए, पुनरावृत्ति और पुनरावृत्ति।

  • पुनरावृति: लूप की शुरुआत में एक छलांग
  • पुनरावृत्ति: बुलाया समारोह की शुरुआत करने के लिए एक छलांग

आप देखते हैं कि यहां मतभेदों के लिए बहुत जगह नहीं है।

(मुझे लगता है कि पुनरावर्तन एक टेल-कॉल और कंपाइलर है जो उस अनुकूलन के बारे में जानते हैं)।


9

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

टीएल; डीआर पुनरावर्ती एल्गोरिदम में आम तौर पर चलने वालों की तुलना में एक बदतर कैश व्यवहार होता है।


6

यहां ज्यादातर उत्तर गलत हैं । सही उत्तर यह निर्भर करता है । उदाहरण के लिए, यहां दो सी फ़ंक्शन हैं जो एक पेड़ से चलते हैं। पहले पुनरावर्ती एक:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

और यहाँ एक ही फ़ंक्शन को पुनरावृत्ति का उपयोग करके कार्यान्वित किया गया है:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

कोड के विवरण को समझना महत्वपूर्ण नहीं है। बस यही pनोड्स हैं और यही P_FOR_EACH_CHILDचलना है। पुनरावृत्त संस्करण में हमें एक स्पष्ट स्टैक की आवश्यकता होती है, stजिस पर नोड्स को धक्का दिया जाता है और फिर पॉपअप और हेरफेर किया जाता है।

पुनरावर्ती फ़ंक्शन पुनरावृत्तियों की तुलना में बहुत तेज़ी से चलता है। इसका कारण यह है कि उत्तरार्द्ध में, प्रत्येक आइटम के CALLलिए, फ़ंक्शन को ए st_pushकी जरूरत है और फिर दूसरे को st_pop

पूर्व में, आपके पास केवल CALLप्रत्येक नोड के लिए पुनरावर्ती है।

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

सावधान अनुकूलन के साथ, जैसे कि इनलाइनिंग st_pushऔर st_pop, मैं पुनरावर्ती दृष्टिकोण के साथ लगभग समता तक पहुंच सकता हूं। लेकिन कम से कम मेरे कंप्यूटर पर, रिकर्सिव कॉल की लागत की तुलना में हीप मेमोरी तक पहुंचने की लागत बड़ी है।

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


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

1
7 नोड बाइनरी ट्री 10 ^ 8 बार का प्री-ऑर्डर ट्रैवर्सल किया। पुनरावृत्ति 25 नं। स्पष्ट स्टैक (बाउंड-चेक किया गया या नहीं - इससे कोई फर्क नहीं पड़ता) ~ 15ns। पुनरावर्तन को केवल धकेलने और कूदने के अतिरिक्त (रजिस्टर सेविंग एंड रिस्टोरेशन + (आमतौर पर) स्ट्रिकटर फ्रेम एलाइनमेंट्स) रजिस्टर करने की आवश्यकता है। (और यह गतिशील रूप से जुड़े लिबास में PLT के साथ खराब हो जाता है।) आपको स्पष्ट स्टैक को ढेर-आवंटित करने की आवश्यकता नहीं है। आप एक बाधा कर सकते हैं जिसका पहला फ्रेम नियमित कॉल स्टैक पर है ताकि आप सबसे आम मामले के लिए कैश लोकल का त्याग न करें जहां आप पहले ब्लॉक से अधिक नहीं हैं।
PSkocik

3

सामान्य तौर पर, नहीं, पुनरावृत्ति किसी भी यथार्थवादी उपयोग में लूप से तेज नहीं होगी जिसमें दोनों रूपों में व्यवहार्य कार्यान्वयन हो। मेरा मतलब है, निश्चित रूप से, आप उन छोरों को कोड कर सकते हैं जो हमेशा के लिए लेते हैं, लेकिन उसी लूप को लागू करने के बेहतर तरीके होंगे जो पुनरावृत्ति के माध्यम से उसी समस्या के किसी भी कार्यान्वयन को बेहतर बना सकते हैं।

आपने सिर पर कील ठोंक दी थी कारण के बारे में; स्टैक फ्रेम बनाना और नष्ट करना एक साधारण कूद की तुलना में अधिक महंगा है।

हालाँकि, ध्यान दें कि मैंने कहा "दोनों रूपों में व्यवहार्य कार्यान्वयन है"। कई सॉर्टिंग एल्गोरिदम जैसी चीजों के लिए, उन्हें लागू करने का एक बहुत व्यवहार्य तरीका नहीं है, जो प्रभावी रूप से एक स्टैक के अपने संस्करण को सेट नहीं करता है, क्योंकि बच्चे "कार्य" की प्रक्रिया के कारण स्वाभाविक रूप से प्रक्रिया का हिस्सा हैं। इस प्रकार, पुनरावृत्ति केवल लूपिंग के माध्यम से एल्गोरिथ्म को लागू करने के प्रयास के रूप में तेज़ हो सकती है।

संपादित करें: यह उत्तर गैर-कार्यात्मक भाषाओं को मान रहा है, जहां अधिकांश बुनियादी डेटा प्रकार परस्पर हैं। यह कार्यात्मक भाषाओं पर लागू नहीं होता है।


यही कारण है कि पुनरावृत्ति के कई मामलों को अक्सर उन भाषाओं में संकलक द्वारा अनुकूलित किया जाता है जहां पुनरावृत्ति अक्सर उपयोग की जाती है। उदाहरण के लिए, F # में,। To op opcode के साथ पुनरावर्ती कार्यों के लिए पूर्ण समर्थन के अलावा, आप अक्सर लूप के रूप में संकलित एक पुनरावर्ती फ़ंक्शन को देखते हैं।
em70

हां। पूंछ पुनरावृत्ति कभी-कभी दोनों दुनिया का सबसे अच्छा हो सकता है - एक पुनरावर्ती कार्य को लागू करने के लिए कार्यात्मक "उपयुक्त" तरीका, और लूप का उपयोग करने का प्रदर्शन।
एम्बर

1
यह सामान्य रूप से सही नहीं है। कुछ वातावरणों में, उत्परिवर्तन (जो जीसी के साथ बातचीत करता है) पूंछ पुनरावृत्ति की तुलना में अधिक महंगा है, जो आउटपुट में एक सरल लूप में बदल जाता है जो एक अतिरिक्त स्टैक फ्रेम का उपयोग नहीं करता है।
डिट्रिच एप्प

2

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


1

कार्यात्मक प्रोग्रामिंग " कैसे " के बजाय " क्या " के बारे में अधिक है ।

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

एक प्रोग्रामर दृष्टिकोण से अधिक मायने रखता है पहली जगह में अनुकूलन के बजाय पठनीयता और स्थिरता है। फिर, "समय से पहले अनुकूलन सभी बुराई की जड़ है"।


0

यह एक अनुमान है। आम तौर पर पुनर्संयोजन शायद लूपिंग को अक्सर या कभी भी सभ्य आकार की समस्याओं पर हरा नहीं करता है यदि दोनों वास्तव में अच्छे एल्गोरिदम का उपयोग कर रहे हैं (कार्यान्वयन की कठिनाई की गिनती नहीं कर रहे हैं), यह भिन्न हो सकता है यदि भाषा w / टेल कॉल पुनरावर्तन (और एक पूंछ पुनरावर्ती एल्गोरिथ्म ) के साथ उपयोग किया जाता है और भाषा के भाग के रूप में लूप के साथ) -जिसमें शायद बहुत कुछ समान होगा और संभवतया कुछ समय के लिए पुनरावृत्ति पसंद करेंगे।


0

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

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }

1
बिग ओ "आनुपातिक" है। तो दोनों हैं O(n), लेकिन एक दूसरे से ज्यादा समय ले सकता हैx , सभी के लिए n
ctrl-alt-delor
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.