पूंछ पुनरावृत्ति वास्तव में कैसे काम करती है?


121

मैं लगभग समझता हूं कि पूंछ पुनरावृत्ति कैसे काम करती है और इसके और एक सामान्य पुनरावृत्ति के बीच अंतर। मैं केवल यह नहीं समझता कि इसके रिटर्न एड्रेस को याद रखने के लिए स्टैक की आवश्यकता क्यों नहीं है।

// tail recursion
int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

int factorial (int n) {
    return fac_times (n, 1);
}

// normal recursion
int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

टेल रिकर्सन फंक्शन में फंक्शन को कॉल करने के बाद कुछ नहीं करना है, लेकिन यह मेरे लिए कोई मतलब नहीं है।


16
पूंछ प्रत्यावर्तन है "सामान्य" प्रत्यावर्तन। इसका मतलब केवल यह है कि फ़ंक्शन के अंत में पुनरावृत्ति होती है।
पीट बेकर

7
... लेकिन इसे आईएल स्तर पर सामान्य पुनरावृत्ति की तुलना में एक अलग तरीके से लागू किया जा सकता है, स्टैक की गहराई को कम कर सकता है।
कीथ्स

2
BTW, gcc यहां "सामान्य" उदाहरण पर पूंछ पुनरावृत्ति उन्मूलन कर सकता है।
dmckee --- पूर्व-संचालक ने बिल्ली

1
@ गीक - मैं एक सी # देव हूं, इसलिए मेरी "असेंबली लैंग्वेज" एमएसआईएल या सिर्फ आईएल है। C / C ++ के लिए, IL को ASM से बदलें।
कीथ्स

1
@ShannonSeverance मैंने पाया कि gcc यह बिना किसी सक्षम विधानसभा कोड के साधारण समीक्षक द्वारा जाँच कर रहा है -O3। लिंक पहले की चर्चा के लिए है जो बहुत समान जमीन को कवर करता है और चर्चा करता है कि इस अनुकूलन को लागू करने के लिए क्या आवश्यक है।
dmckee --- पूर्व-संचालक ने Kitten

जवाबों:


169

कंपाइलर बस इसे बदलने में सक्षम है

int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

कुछ इस तरह से:

int fac_times (int n, int acc) {
label:
    if (n == 0) return acc;
    acc *= n--;
    goto label;
}

2
@ Mr.32 मुझे आपका सवाल समझ नहीं आ रहा है। मैंने फ़ंक्शन को एक समतुल्य में बदल दिया, लेकिन बिना स्पष्ट पुनरावर्तन (जो स्पष्ट फ़ंक्शन कॉल के बिना)। यदि आप तर्क को कुछ गैर-समकक्ष में बदलते हैं, तो आप वास्तव में कुछ या सभी मामलों में फ़ंक्शन लूप को हमेशा के लिए बना सकते हैं।
एलेक्सी फ्रुंज़े

18
तो पूंछ पुनरावर्तन केवल संकलक के अनुकूलन के कारण प्रभावी है? और अन्यथा यह स्टैक मेमोरी वार के संदर्भ में सामान्य पुनरावृत्ति के समान होगा?
एलन कोरोमेनो

34
हां। यदि कंपाइलर लूप में पुनरावृत्ति को कम नहीं कर सकता है, तो आप पुनरावृत्ति के साथ फंस गए हैं। सभी या कुछ भी नहीं।
एलेक्सी फ्रुंज़े

3
@AlanDert: सही है। आप पूंछ पुनरावृत्ति को "पूंछ कॉल अनुकूलन" का एक विशेष मामला भी मान सकते हैं, विशेष क्योंकि पूंछ कॉल उसी फ़ंक्शन के लिए होती है। सामान्य तौर पर, पूंछ पुनरावृत्ति पर लागू होने के रूप में किसी भी पूंछ कॉल ("कोई काम नहीं करना छोड़ दिया" पर समान आवश्यकताओं के साथ, और जहां पूंछ कॉल का रिटर्न मूल्य सीधे वापस आ जाता है) को अनुकूलित किया जा सकता है अगर कंपाइलर कॉल को एक में कर सकता है जिस तरह से टेल कॉल करने वाले फ़ंक्शन के रिटर्न एड्रेस होने के लिए कॉल किए गए फंक्शन का रिटर्न एड्रेस सेट किया जाता है, जिस एड्रेस से टेल कॉल की गई थी।
स्टीव जेसप

1
C में @AlanDert यह केवल एक अनुकूलन है जो किसी भी मानक द्वारा लागू नहीं किया गया है, इसलिए पोर्टेबल कोड इस पर निर्भर नहीं होना चाहिए। लेकिन भाषाएं हैं (योजना एक उदाहरण है), जहां पूंछ पुनरावृत्ति अनुकूलन मानक द्वारा लागू किया जाता है, इसलिए आपको चिंता करने की ज़रूरत नहीं है कि यह कुछ वातावरणों में अतिप्रवाह को ढेर कर देगा।
जन व्रोबेल

57

आप पूछते हैं कि "इसके रिटर्न एड्रेस को याद रखने के लिए स्टैक की आवश्यकता क्यों नहीं है"।

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

इसके विपरीत, टेल कॉल ऑप्टिमाइज़ेशन के बिना:

f: ...
   CALL g
   RET
g:
   ...
   RET

इस मामले में, जब gबुलाया जाता है, तो स्टैक ऐसा दिखेगा:

   SP ->  Return address of "g"
          Return address of "f"

दूसरी ओर, पूंछ कॉल अनुकूलन के साथ:

f: ...
   JUMP g
g:
   ...
   RET

इस मामले में, जब gबुलाया जाता है, तो स्टैक ऐसा दिखेगा:

   SP ->  Return address of "f"

स्पष्ट रूप से, जब gवापस आएगा, यह उस स्थान पर वापस आ जाएगा जहां fसे बुलाया गया था।

EDIT : उपरोक्त उदाहरण उस मामले का उपयोग करता है जहां एक फ़ंक्शन दूसरे फ़ंक्शन को कॉल करता है। जब फ़ंक्शन स्वयं कॉल करता है, तो तंत्र समान होता है।


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

12

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

// tail recursion
int fac_times (int n, int acc = 1) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

जैसे कुछ संकलन होगा

// accumulator
int fac_times (int n) {
    int acc = 1;
    while (n > 0) {
        acc *= n;
        n -= 1;
    }
    return acc;
}

3
एलेक्सी के कार्यान्वयन के रूप में चालाक नहीं ... और हां यह एक तारीफ है।
मैथ्यू एम।

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

यदि यह सब पूंछ पुनरावृत्ति है, तो लोग इसे लेकर इतने उत्साहित क्यों हैं? मैं किसी को लूप्स के बारे में उत्साहित होते हुए नहीं देखता।
बुह बुह

@BuhBuh: इसमें स्टैकओवरफ्लो नहीं है, और स्टैक पुश / मापदंडों की पॉपिंग से बचा जाता है। इस तरह एक तंग पाश के लिए यह अंतर की दुनिया बना सकता है। इसके अलावा लोगों को उत्साहित नहीं होना चाहिए।
मूविंग डक

11

दो तत्व हैं जो एक पुनरावर्ती कार्य में मौजूद होने चाहिए:

  1. पुनरावर्ती कॉल
  2. वापसी मूल्यों की गिनती रखने के लिए एक जगह।

स्टैक फ्रेम में एक "नियमित" पुनरावर्ती कार्य रहता है (2)।

नियमित पुनरावर्ती फ़ंक्शन में वापसी मान दो प्रकार के मानों से बने होते हैं:

  • अन्य रिटर्न मान
  • फ़ंक्शन गणना के स्वामी का परिणाम

आइए अपने उदाहरण देखें:

int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

उदाहरण के लिए, फ्रेम एफ (5) "संगणक" का परिणाम है, इसका अभिकलन (5) और एफ (4) का मूल्य है। यदि मैं फैक्टरियल (5) कहता हूं, इससे पहले कि स्टैक कॉल ध्वस्त हो जाए, मेरे पास है:

 [Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]

ध्यान दें कि प्रत्येक स्टैक स्टोर, मेरे द्वारा उल्लिखित मानों के अलावा, फ़ंक्शन का पूरा दायरा। तो, एक पुनरावर्ती फ़ंक्शन f के लिए मेमोरी का उपयोग O (x) है, जहां x को पुनरावर्ती कॉल की संख्या है जो मुझे करना है। तो, अगर मैं factorial (1) या factorial (2) की गणना करने के लिए RAM की 1kb की आवश्यकता है, तो मुझे factorial (100) की गणना करने के लिए ~ 100k की आवश्यकता है, और इसी तरह।

एक पुनरावृत्ति समारोह डाल (2) यह तर्क में है।

एक पूंछ पुनरावृत्ति में, मैं प्रत्येक पुनरावर्ती फ्रेम में आंशिक गणनाओं के परिणाम को अगले एक पैरामीटर का उपयोग करके पास करता हूं। आइए देखें हमारे तथ्यात्मक उदाहरण, टेल पुनरावर्ती:

int factorial (इंट n) {इंट हेल्पर (int num, इंट जमा) {if num == 0 जमा हुआ रिटर्न अन्य हेल्पर (num - 1, जमा * num)} रिटर्न हेल्पर (n, 1)
}

आइए देखें कि यह फैक्टरियल में फ्रेम है (4):

[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]

अंतर देखें? "नियमित" पुनरावर्ती कॉल में रिटर्न फ़ंक्शंस पुनरावर्ती रूप से अंतिम मान बनाता है। टेल रिकर्सियन में वे केवल आधार मामले (पिछले एक मूल्यांकन) का संदर्भ देते हैं । हम संचायक को उस तर्क को कहते हैं जो पुराने मूल्यों पर नज़र रखता है।

पुनरावर्तन टेम्पलेट

नियमित पुनरावर्ती कार्य इस प्रकार है:

type regular(n)
    base_case
    computation
    return (result of computation) combined with (regular(n towards base case))

एक पूंछ पुनरावृत्ति में इसे बदलने के लिए हम:

  • एक सहायक फ़ंक्शन का परिचय करें जो संचायक को वहन करता है
  • मुख्य फ़ंक्शन के अंदर सहायक फ़ंक्शन को चलाएं, बेस केस पर संचायक सेट के साथ।

देखो:

type tail(n):
    type helper(n, accumulator):
        if n == base case
            return accumulator
        computation
        accumulator = computation combined with accumulator
        return helper(n towards base case, accumulator)
    helper(n, base case)

फर्क देखें?

टेल कॉल ऑप्टिमाइज़ेशन

चूंकि टेल कॉल स्टैक के गैर-सीमा-मामलों पर कोई राज्य संग्रहीत नहीं किया जा रहा है, इसलिए वे इतने महत्वपूर्ण नहीं हैं। कुछ भाषाओं / व्याख्याकारों ने पुराने स्टैक को नए के साथ स्थानापन्न किया। इसलिए, कोई स्टैक फ्रेम कॉल की संख्या को बाधित करने के साथ, टेल कॉल इन मामलों में फॉर-लूप की तरह व्यवहार करता है

इसे अनुकूलित करने के लिए यह आपके कंपाइलर पर निर्भर है, या नहीं।


6

यहाँ एक सरल उदाहरण दिया गया है जो बताता है कि पुनरावर्ती कार्य कैसे कार्य करते हैं:

long f (long n)
{

    if (n == 0) // have we reached the bottom of the ocean ?
        return 0;

    // code executed in the descendence

    return f(n-1) + 1; // recurrence

    // code executed in the ascendence

}

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


1

रिकर्सिव फंक्शन एक ऐसा फंक्शन है जो खुद से कॉल करता है

यह प्रोग्रामर को कम से कम कोड का उपयोग करके कुशल प्रोग्राम लिखने की अनुमति देता है ।

नकारात्मक पक्ष यह है कि वे ठीक से नहीं लिखे जाने पर अनंत छोरों और अन्य अप्रत्याशित परिणामों का कारण बन सकते हैं

मैं Simple Recursive function और Tail Recursive function दोनों की व्याख्या करूँगा

एक साधारण पुनरावर्ती कार्य लिखने के लिए

  1. विचार करने के लिए पहला बिंदु यह है कि आपको लूप से बाहर आने का फैसला करना चाहिए जो अगर लूप है
  2. दूसरा यह है कि अगर हम अपने स्वयं के कार्य करते हैं तो क्या करना है

दिए गए उदाहरण से:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

उपरोक्त उदाहरण से

if(n <=1)
     return 1;

पाश से बाहर निकलने के लिए निर्णायक कारक है

else 
     return n * fact(n-1);

क्या वास्तविक प्रसंस्करण किया जाना है

मुझे आसानी से समझने के लिए एक-एक करके टास्क को तोड़ना चाहिए।

आइए देखते हैं कि अगर मैं दौड़ता हूं तो आंतरिक रूप से क्या होता है fact(4)

  1. उपादान n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifलूप विफल हो जाता है elseइसलिए यह लूप में जाता है इसलिए यह वापस आ जाता है4 * fact(3)

  1. स्टैक मेमोरी में, हमारे पास है 4 * fact(3)

    प्रतिस्थापित करना = ३

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Ifलूप विफल रहता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है 3 * fact(2)

याद रखें कि हमने `` `4 * तथ्य (3)` `कहा

के लिए उत्पादन fact(3) = 3 * fact(2)

अब तक ढेर है 4 * fact(3) = 4 * 3 * fact(2)

  1. स्टैक मेमोरी में, हमारे पास है 4 * 3 * fact(2)

    प्रतिस्थापित करना = २

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Ifलूप विफल रहता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है 2 * fact(1)

याद है हमने बुलाया 4 * 3 * fact(2)

के लिए उत्पादन fact(2) = 2 * fact(1)

अब तक ढेर है 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. स्टैक मेमोरी में, हमारे पास है 4 * 3 * 2 * fact(1)

    प्रतिस्थापित करना = १

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If पाश सत्य है

तो यह लौट आता है 1

याद है हमने बुलाया 4 * 3 * 2 * fact(1)

के लिए उत्पादन fact(1) = 1

अब तक ढेर है 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

अंत में, तथ्य (4) = 4 * 3 * 2 * 1 = 24 का परिणाम

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

पूंछ Recursion होगा

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}
  1. उपादान n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifलूप विफल हो जाता है elseइसलिए यह लूप में जाता है इसलिए यह वापस आ जाता हैfact(3, 4)

  1. स्टैक मेमोरी में, हमारे पास है fact(3, 4)

    प्रतिस्थापित करना = ३

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Ifलूप विफल रहता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है fact(2, 12)

  1. स्टैक मेमोरी में, हमारे पास है fact(2, 12)

    प्रतिस्थापित करना = २

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Ifलूप विफल रहता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है fact(1, 24)

  1. स्टैक मेमोरी में, हमारे पास है fact(1, 24)

    प्रतिस्थापित करना = १

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If पाश सत्य है

तो यह लौट आता है running_total

के लिए उत्पादन running_total = 24

अंत में, तथ्य (4,1) = 24 का परिणाम

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


0

मेरा उत्तर अनुमान से अधिक है, क्योंकि पुनरावृत्ति आंतरिक कार्यान्वयन से संबंधित है।

पूंछ पुनरावर्तन में, पुनरावर्ती कार्य को उसी फ़ंक्शन के अंत में कहा जाता है। संभवतः संकलक नीचे तरीके से अनुकूलित कर सकते हैं:

  1. चल समारोह को हवा दें (यानी इस्तेमाल किया गया स्टैक वापस बुलाया गया है)
  2. उन चरों को संग्रहीत करें जिनका उपयोग अस्थायी भंडारण में कार्य करने के लिए तर्कों के रूप में किया जा रहा है
  3. इसके बाद, अस्थायी रूप से संग्रहीत तर्क के साथ फ़ंक्शन को फिर से कॉल करें

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

लेकिन मेरा मानना ​​है कि यदि फंक्शन के अंदर डिस्ट्रक्टर्स को बुलाया जाए तो यह ऑप्टिमाइज़ेशन लागू नहीं हो सकता है।


0

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

void tail(int i) {
    if(i<=0) return;
    else {
     system.out.print(i+"");
     tail(i-1);
    }
   }

ऑप्टिमाइज़ेशन करने के बाद, उपरोक्त कोड को एक में बदल दिया जाता है।

void tail(int i) {
    blockToJump:{
    if(i<=0) return;
    else {
     system.out.print(i+"");
     i=i-1;
     continue blockToJump;  //jump to the bolckToJump
    }
    }
   }

इस तरह से कम्पाइलर टेल रिसर्शन ऑप्टिमाइजेशन करता है।

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