पूंछ कॉल अनुकूलन क्या है?


817

बहुत सरलता से, टेल-कॉल ऑप्टिमाइज़ेशन क्या है?

अधिक विशेष रूप से, कुछ छोटे कोड स्निपेट क्या हैं, जहां इसे लागू किया जा सकता है, और क्यों नहीं, स्पष्टीकरण के साथ क्यों?


10
TCO एक गोमो, एक छलांग में पूंछ की स्थिति में एक फ़ंक्शन कॉल करता है।
विल नेस

8
); यह सवाल 8 साल है कि एक से पहले पूरी तरह से कहा गया था
majelbstoat

जवाबों:


754

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

योजना कुछ प्रोग्रामिंग भाषाओं में से एक है जो इस बात की गारंटी देती है कि किसी भी कार्यान्वयन को यह अनुकूलन प्रदान करना होगा (जावास्क्रिप्ट भी करता है, ईएस 6 से शुरू होता है) , इसलिए यहां योजना में फैक्टरियल फ़ंक्शन के दो उदाहरण हैं:

(define (fact x)
  (if (= x 0) 1
      (* x (fact (- x 1)))))

(define (fact x)
  (define (fact-tail x accum)
    (if (= x 0) accum
        (fact-tail (- x 1) (* x accum))))
  (fact-tail x 1))

पहला फ़ंक्शन पूंछ पुनरावर्ती नहीं है क्योंकि जब पुनरावर्ती कॉल किया जाता है, तो फ़ंक्शन को कॉल रिटर्न के बाद परिणाम के साथ गुणा करने का ट्रैक रखने की आवश्यकता होती है। इस प्रकार, स्टैक निम्नानुसार दिखता है:

(fact 3)
(* 3 (fact 2))
(* 3 (* 2 (fact 1)))
(* 3 (* 2 (* 1 (fact 0))))
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6

इसके विपरीत, पूंछ पुनरावर्ती फैक्टरियल के लिए स्टैक ट्रेस निम्नानुसार है:

(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6

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


99
यदि आप इसके बारे में अधिक जानना चाहते हैं, तो मैं कंप्यूटर प्रोग्राम के स्ट्रक्चर और इंटरप्रिटेशन के पहले अध्याय को पढ़ने का सुझाव देता हूं।
काइल क्रोनिन

3
महान जवाब, पूरी तरह से समझाया।
योना

15
कड़ाई से बोलते हुए, टेल कॉल ऑप्टिमाइज़ेशन कॉलर के स्टैक फ्रेम को बछड़ों के साथ बदलने की आवश्यकता नहीं है, बल्कि, यह सुनिश्चित करता है कि पूंछ की स्थिति में एक अनबाउंड संख्या में कॉल को केवल एक सीमित स्थान की आवश्यकता होती है। देखें क्लिंजर का पेपर "प्रॉपर टेल रिकर्सन एंड स्पेस एफिशिएंसी
जॉन

3
क्या यह निरंतर-अंतरिक्ष तरीके से पुनरावर्ती कार्यों को लिखने का एक तरीका है? क्योंकि आप पुनरावृत्त दृष्टिकोण का उपयोग करके समान परिणाम प्राप्त नहीं कर सकते थे?
dclowd9901

5
@ dclowd9901, TCO आपको एक कार्यात्मक शैली पसंद करने की अनुमति देता है, ताकि एक पुनरावृत्त लूप हो। आप अनिवार्य शैली पसंद कर सकते हैं। कई भाषाएं (जावा, पायथन) TCO प्रदान नहीं करती हैं, तो आपको यह जानना होगा कि एक कार्यात्मक कॉल मेमोरी की लागत ... और अनिवार्य शैली पसंद की जाती है।
mcoolive

551

आइए एक सरल उदाहरण के माध्यम से चलते हैं: सी में कार्यान्वित तथ्यात्मक फ़ंक्शन।

हम स्पष्ट पुनरावर्ती परिभाषा के साथ शुरू करते हैं

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    return n * fac(n - 1);
}

एक फ़ंक्शन टेल कॉल के साथ समाप्त होता है यदि फ़ंक्शन रिटर्न से पहले अंतिम ऑपरेशन एक अन्य फ़ंक्शन कॉल है। यदि यह कॉल समान फ़ंक्शन को आमंत्रित करता है, तो यह पूंछ-पुनरावर्ती है।

भले ही fac()पहली नज़र में पूंछ-पुनरावृत्ति दिखती है, यह वैसा नहीं है जैसा वास्तव में होता है

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    unsigned acc = fac(n - 1);
    return n * acc;
}

यानी अंतिम ऑपरेशन गुणन है न कि फ़ंक्शन कॉल।

हालाँकि, fac()कॉल चेन को एक अतिरिक्त तर्क के रूप में संचित मूल्य से कम करके और वापसी परिणाम के रूप में केवल अंतिम परिणाम को फिर से पारित करके पूंछ-पुनरावर्ती होना फिर से लिखना संभव है :

unsigned fac(unsigned n)
{
    return fac_tailrec(1, n);
}

unsigned fac_tailrec(unsigned acc, unsigned n)
{
    if (n < 2) return acc;
    return fac_tailrec(n * acc, n - 1);
}

अब, यह क्यों उपयोगी है? चूँकि हम तुरंत टेल कॉल के बाद वापस लौट आते हैं, हम टेल स्टैक में फ़ंक्शन को लागू करने से पहले पिछले स्टैकफ्रेम को त्याग सकते हैं, या पुनरावर्ती कार्यों के मामले में, स्टैकफ्रेम को पुन: उपयोग कर सकते हैं।

टेल-कॉल ऑप्टिमाइज़ेशन हमारे पुनरावर्ती कोड को बदल देता है

unsigned fac_tailrec(unsigned acc, unsigned n)
{
TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

इसमें इनबिल्ट किया जा सकता है fac()और हम पहुंचते हैं

unsigned fac(unsigned n)
{
    unsigned acc = 1;

TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

जो के बराबर है

unsigned fac(unsigned n)
{
    unsigned acc = 1;

    for (; n > 1; --n)
        acc *= n;

    return acc;
}

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


क्या आप बता सकते हैं कि स्टैकफ्रेम का क्या मतलब है? क्या कॉल स्टैक और स्टैकफ्रेम के बीच अंतर है?
शसक

10
@ कासा: एक स्टैक फ्रेम कॉल स्टैक का हिस्सा होता है जो किसी दिए गए (सक्रिय) फ़ंक्शन के लिए होता है; cf en.wikipedia.org/wiki/Call_stack#Structure
Christoph

1
मैंने इस पोस्ट को पढ़ने के बाद 2ety.com/2015/06/tail-call-optimization.html
agm1984

198

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

यह अनुकूलन बार-बार विस्फोट होने के बजाय पुनरावर्ती कॉल निरंतर स्टैक स्थान ले सकता है।

उदाहरण: यह तथ्यात्मक कार्य TCOptimizable नहीं है:

def fact(n):
    if n == 0:
        return 1
    return n * fact(n-1)

यह फ़ंक्शन अपने रिटर्न स्टेटमेंट में कॉल के अलावा एक अन्य फ़ंक्शन करता है।

यह निम्न कार्य TCOptimizable है:

def fact_h(n, acc):
    if n == 0:
        return acc
    return fact_h(n-1, acc*n)

def fact(n):
    return fact_h(n, 1)

ऐसा इसलिए है क्योंकि इनमें से किसी भी फ़ंक्शन में होने वाली अंतिम बात किसी अन्य फ़ंक्शन को कॉल करना है।


3
पूरे 'फंक्शन जी हो सकता है f' बात थोड़ी भ्रमित करने वाली थी, लेकिन मुझे वही मिलता है जो आपका मतलब है, और उदाहरणों ने वास्तव में चीजों को स्पष्ट किया है। आपका बहुत बहुत धन्यवाद!
मेज़लबस्तोत

10
उत्कृष्ट उदाहरण जो अवधारणा को दर्शाता है। बस ध्यान रखें कि आपके द्वारा चुनी गई भाषा में टेल कॉल एलिमिनेशन या टेल कॉल ऑप्टिमाइज़ेशन को लागू करना है। उदाहरण में, पायथन में लिखा गया है, यदि आप 1000 का मान दर्ज करते हैं, तो आपको "रनटाइमटाइम: अधिकतम पुनरावृत्ति की गहराई अधिक हो जाती है" क्योंकि डिफ़ॉल्ट पायथन कार्यान्वयन टेल रिसर्शन उन्मूलन का समर्थन नहीं करता है। स्वयं Guido की एक पोस्ट देखें कि यह क्यों समझाया गया है: neopythonic.blogspot.pt/2009/04/tail-recursion-elimination.html
rmcc 17

" एकमात्र स्थिति" थोड़ी बहुत निरपेक्ष है; टीआरएमसी भी है , कम से कम सिद्धांत में, जो उसी तरह से (cons a (foo b))या (+ c (bar d))पूंछ की स्थिति में अनुकूलित होगा ।
विल नेस

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

मुझे लगता है कि आप TCOptimized मतलब है। यह कहते हुए कि यह TCOptimizable शिशु नहीं है कि इसे कभी भी अनुकूलित नहीं किया जा सकता है (जब यह वास्तव में हो सकता है)
जैक्स मैथ्यू

65

संभवतः पूंछ कॉल, पुनरावर्ती पूंछ कॉल और पूंछ कॉल अनुकूलन के लिए मुझे मिला सबसे अच्छा उच्च स्तरीय विवरण ब्लॉग पोस्ट है

"बिल्ली क्या है: एक पूंछ कॉल"

डैन सुगल्स्की द्वारा। पूंछ कॉल अनुकूलन पर वह लिखते हैं:

एक पल के लिए, इस सरल कार्य पर विचार करें:

sub foo (int a) {
  a += 15;
  return bar(a);
}

तो, आप या आपकी भाषा संकलक क्या कर सकते हैं, करते हैं? खैर, यह क्या कर सकता है फार्म return somefunc();के निचले स्तर के अनुक्रम में बारी है pop stack frame; goto somefunc();। हमारे उदाहरण में, इसका मतलब है कि हम कॉल करने से पहले bar, fooखुद को साफ करते हैं और फिर, barसबरूटीन के रूप में कॉल करने के बजाय , हम gotoशुरू करने के लिए एक निम्न-स्तरीय ऑपरेशन करते हैं barFooयह पहले से ही स्टैक से खुद को साफ कर barलेता है , इसलिए जब यह शुरू होता है तो ऐसा लगता है कि जिसने भी कॉल fooकिया है उसे वास्तव में कॉल किया गया है bar, और जब barउसका मूल्य लौटाता है, तो वह इसे सीधे लौटा देता है foo, जिसे कॉल किया जाता है , न कि इसे वापस करने के बजाय fooजिसे वह अपने कॉलर को वापस कर देगा।

और पूंछ पुनरावृत्ति पर:

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

ताकि यह:

sub foo (int a, int b) {
  if (b == 1) {
    return a;
  } else {
    return foo(a*a + a, b - 1);
  }

चुपचाप में बदल जाता है:

sub foo (int a, int b) {
  label:
    if (b == 1) {
      return a;
    } else {
      a = a*a + a;
      b = b - 1;
      goto label;
   }

इस विवरण के बारे में मुझे क्या पसंद है, यह अनिवार्य भाषा की पृष्ठभूमि (सी, सी ++, जावा) से आने वाले लोगों के लिए कितना आसान और आसान है


4
404 त्रुटि। हालाँकि, यह अभी भी आर्काइव.ऑर्ग: web.archive.org/web/20111030134120/http://www.sidhe.org/~dan/…
टॉमी

मुझे यह नहीं मिला, प्रारंभिक fooकार्य पूंछ कॉल अनुकूलित नहीं है? यह केवल एक फ़ंक्शन को अपने अंतिम चरण के रूप में बुला रहा है, और यह केवल उस मूल्य को वापस कर रहा है, है ना?
21

1
@TryinHard शायद आपके दिमाग में नहीं था, लेकिन मैंने इसे अपडेट किया कि यह किस बारे में है। क्षमा करें, पूरे लेख को दोहराने के लिए नहीं जा रहा है!
btiernay

2
धन्यवाद, यह सबसे अप-वोट्ड स्कीम उदाहरण की तुलना में सरल और अधिक समझने योग्य है (उल्लेख करने के लिए नहीं, स्कीम एक आम भाषा नहीं है जिसे अधिकांश डेवलपर्स समझते हैं)
सेविन 7

2
जैसा कि कोई है जो शायद ही कभी कार्यात्मक भाषाओं में गोता लगाता है, यह "मेरी बोली" में स्पष्टीकरण देखने के लिए संतुष्टिदायक है। कार्यात्मक प्रोग्रामर के लिए अपनी पसंद की भाषा में प्रचार करने के लिए एक (समझने योग्य) प्रवृत्ति है, लेकिन अनिवार्य दुनिया से आने पर मुझे इस तरह से एक उत्तर के चारों ओर अपना सिर लपेटना इतना आसान लगता है।
जेम्स बेनिंजर

15

सबसे पहले ध्यान दें कि सभी भाषाएं इसका समर्थन नहीं करती हैं।

TCO पुनरावृत्ति के एक विशेष मामले पर लागू होता है। इसका सार यह है, यदि किसी कार्य में आप जो अंतिम कार्य करते हैं, वह स्वयं कॉल होता है (जैसे कि यह "टेल" स्थिति से स्वयं को कॉल कर रहा है), इसे कंपाइलर द्वारा मानक पुनरावृत्ति के बजाय पुनरावृत्ति की तरह कार्य करने के लिए अनुकूलित किया जा सकता है।

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


3
टेल कॉल गैर-पुनरावर्ती कार्यों पर भी लागू हो सकता है। कोई भी फ़ंक्शन जिसकी वापसी से पहले अंतिम गणना दूसरे फ़ंक्शन के लिए एक कॉल है, एक टेल कॉल का उपयोग कर सकता है।
ब्रायन

जरूरी नहीं कि भाषा के आधार पर किसी भाषा पर - 64 बिट C # कंपाइलर में टेल ऑपकोड डाला जा सकता है जबकि 32-बिट संस्करण नहीं होगा; और F # रिलीज़ बिल्ड होगा, लेकिन F # डिबग डिफ़ॉल्ट रूप से नहीं होगा।
५३ पर स्टीव गिलहम ०

3
"TCO पुनरावृत्ति के एक विशेष मामले पर लागू होता है"। मुझे डर है कि पूरी तरह से गलत है। टेल कॉल पूंछ की स्थिति में किसी भी कॉल पर लागू होती है। आम तौर पर पुनरावृत्ति के संदर्भ में चर्चा की गई थी, लेकिन वास्तव में पुनरावृत्ति के साथ कुछ नहीं करना था।
जॉन हेरोप

@ ब्रायन, ऊपर दिए गए लिंक @btiernay को देखें। क्या प्रारंभिक fooविधि पूंछ कॉल अनुकूलित नहीं है?
सेक्सीबीस्ट

13

X86 डिस्सैस विश्लेषण के साथ जीसीसी न्यूनतम रननीय उदाहरण

आइए देखें कि जीसीसी स्वचालित रूप से उत्पन्न विधानसभा को देखकर हमारे लिए पूंछ कॉल अनुकूलन कैसे कर सकता है।

यह https://stackoverflow.com/a/9814654/895245 जैसे अन्य उत्तरों में उल्लिखित एक अत्यंत ठोस उदाहरण के रूप में काम करेगा कि अनुकूलन पुनरावर्ती फ़ंक्शन कॉल को लूप में बदल सकता है।

यह बदले में स्मृति को बचाता है और प्रदर्शन में सुधार करता है, क्योंकि मेमोरी एक्सेस अक्सर मुख्य चीज होती है जो आजकल कार्यक्रमों को धीमा कर देती है

इनपुट के रूप में, हम GCC को एक गैर-अनुकूलित भोले स्टैक आधारित फैक्टरियल देते हैं:

tail_call.c

#include <stdio.h>
#include <stdlib.h>

unsigned factorial(unsigned n) {
    if (n == 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

int main(int argc, char **argv) {
    int input;
    if (argc > 1) {
        input = strtoul(argv[1], NULL, 0);
    } else {
        input = 5;
    }
    printf("%u\n", factorial(input));
    return EXIT_SUCCESS;
}

गिटहब ऊपर

संकलन और जुदाई:

gcc -O1 -foptimize-sibling-calls -ggdb3 -std=c99 -Wall -Wextra -Wpedantic \
  -o tail_call.out tail_call.c
objdump -d tail_call.out

-foptimize-sibling-callsटेल कॉल के सामान्यीकरण का नाम कहां है man gcc:

   -foptimize-sibling-calls
       Optimize sibling and tail recursive calls.

       Enabled at levels -O2, -O3, -Os.

जैसा कि उल्लेख किया गया है: मैं कैसे जांच करूं कि क्या gcc पूंछ-पुनरावृत्ति अनुकूलन कर रहा है?

मैं चुनता हूं -O1क्योंकि:

  • अनुकूलन के साथ नहीं किया गया है -O0। मुझे संदेह है कि ऐसा इसलिए है क्योंकि आवश्यक मध्यवर्ती रूपांतरण गायब हैं।
  • -O3 बहुत ही शिक्षाप्रद नहीं होगा कि ungodly कुशल कोड पैदा करता है, हालांकि यह भी पूंछ कॉल अनुकूलित है।

साथ डिस्सैडविस्म -fno-optimize-sibling-calls:

0000000000001145 <factorial>:
    1145:       89 f8                   mov    %edi,%eax
    1147:       83 ff 01                cmp    $0x1,%edi
    114a:       74 10                   je     115c <factorial+0x17>
    114c:       53                      push   %rbx
    114d:       89 fb                   mov    %edi,%ebx
    114f:       8d 7f ff                lea    -0x1(%rdi),%edi
    1152:       e8 ee ff ff ff          callq  1145 <factorial>
    1157:       0f af c3                imul   %ebx,%eax
    115a:       5b                      pop    %rbx
    115b:       c3                      retq
    115c:       c3                      retq

के साथ -foptimize-sibling-calls:

0000000000001145 <factorial>:
    1145:       b8 01 00 00 00          mov    $0x1,%eax
    114a:       83 ff 01                cmp    $0x1,%edi
    114d:       74 0e                   je     115d <factorial+0x18>
    114f:       8d 57 ff                lea    -0x1(%rdi),%edx
    1152:       0f af c7                imul   %edi,%eax
    1155:       89 d7                   mov    %edx,%edi
    1157:       83 fa 01                cmp    $0x1,%edx
    115a:       75 f3                   jne    114f <factorial+0xa>
    115c:       c3                      retq
    115d:       89 f8                   mov    %edi,%eax
    115f:       c3                      retq

दोनों के बीच महत्वपूर्ण अंतर यह है कि:

  • का -fno-optimize-sibling-callsउपयोग करता है callq, जो विशिष्ट गैर-अनुकूलित फ़ंक्शन कॉल है।

    यह निर्देश स्टैक पर वापसी पते को बढ़ाता है, इसलिए इसे बढ़ाता है।

    इसके अलावा, यह संस्करण भी करता है push %rbx, जो स्टैक को धक्का देता %rbxहै

    जीसीसी ऐसा इसलिए करता है क्योंकि यह स्टोर करता है edi, जो पहले फ़ंक्शन तर्क ( n) में ebx, फिर कॉल करता है factorial

    जीसीसी को ऐसा करने की आवश्यकता है क्योंकि यह एक और कॉल करने की तैयारी कर रहा है factorial, जो नए का उपयोग करेगा edi == n-1

    यह चुनता है ebxक्योंकि यह रजिस्टर कैली-सेव है: लिनेक्स x86-64 फ़ंक्शन कॉल के माध्यम से कौन से रजिस्टरों को संरक्षित किया जाता है, इसलिए उपकुल factorialइसे बदलने और खोने के लिए नहीं करता है n

  • -foptimize-sibling-callsकिसी भी निर्देश है कि ढेर करने के लिए धक्का का उपयोग नहीं करता: यह केवल करता है gotoके भीतर कूदता factorialके निर्देश के साथ jeऔर jne

    इसलिए, यह संस्करण थोड़ी देर के लूप के बराबर है, बिना किसी फ़ंक्शन के। स्टैक का उपयोग स्थिर है।

उबंटू 18.10, जीसीसी 8.2 में परीक्षण किया गया।


6

इधर देखो:

http://tratt.net/laurie/tech_articles/articles/tail_call_optimization

जैसा कि आप शायद जानते हैं, पुनरावर्ती फ़ंक्शन कॉल एक स्टैक पर कहर बरपा सकते हैं; स्टैक स्पेस से बाहर निकलना आसान है। टेल कॉल ऑप्टिमाइज़ेशन वह तरीका है जिसके द्वारा आप एक पुनरावर्ती शैली एल्गोरिदम बना सकते हैं जो निरंतर स्टैक स्पेस का उपयोग करता है, इसलिए यह बढ़ता नहीं है और बढ़ता है और आपको स्टैक त्रुटियां मिलती हैं।


3
  1. हमें यह सुनिश्चित करना चाहिए कि फंक्शन में कोई गोटो स्टेटमेंट्स न हों .. फंक्शन कॉल द्वारा इस बात का ध्यान रखा जाए कि कैली फंक्शन में आखिरी चीज हो।

  2. बड़े पैमाने पर पुनरावर्ती अनुकूलन के लिए इसका उपयोग कर सकते हैं, लेकिन छोटे पैमाने पर, फ़ंक्शन कॉल को टेल कॉल करने के लिए निर्देश ओवरहेड वास्तविक उद्देश्य को कम करता है।

  3. TCO के कारण हमेशा चलने वाला कार्य हो सकता है:

    void eternity()
    {
        eternity();
    }
    

3 अभी तक अनुकूलित नहीं किया गया है। वह अवलोकित प्रतिनिधित्व है जो संकलक पुनरावृत्ति कोड में बदल जाता है जो पुनरावर्ती कोड के बजाय निरंतर स्टैक स्थान का उपयोग करता है। TCO डेटा संरचना के लिए गलत पुनरावर्तन योजना का उपयोग करने का कारण नहीं है।
नोमेन

"TCO डेटा संरचना के लिए गलत पुनरावर्तन योजना का उपयोग करने का कारण नहीं है" कृपया विस्तृत करें कि यह दिए गए मामले के लिए कैसे प्रासंगिक है। उपरोक्त उदाहरण केवल फ़्रेम का एक उदाहरण बताता है जो TCO के साथ और उसके बिना कॉल स्टैक पर आवंटित किया गया है।
ग्रिलसंदैंड

आपने पारगमन () के लिए निराधार पुनरावर्तन का उपयोग करने का विकल्प चुना। जिसका TCO से कोई लेना-देना नहीं था। अनंत काल टेल-कॉल स्थिति होता है, लेकिन टेल-कॉल स्थिति आवश्यक नहीं है: शून्य अनंत काल () {अनंत काल (); बाहर जाएं(); }
नौ

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

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

3

पुनरावर्ती फ़ंक्शन दृष्टिकोण में एक समस्या है। यह आकार O (n) के एक कॉल स्टैक का निर्माण करता है, जो हमारी कुल मेमोरी लागत O (n) बनाता है। यह एक स्टैक ओवरफ्लो त्रुटि के प्रति संवेदनशील बनाता है, जहां कॉल स्टैक बहुत बड़ा हो जाता है और अंतरिक्ष से बाहर चला जाता है।

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

कई भाषाएँ हैं जो TCO कर रही हैं जैसे (जावास्क्रिप्ट, रूबी और कुछ C) जबकि पायथन और जावा TCO नहीं करते हैं।

जावास्क्रिप्ट भाषा ने :) http://2ality.com/2015/06/tail-call-optimization.html का उपयोग करके पुष्टि की है


0

एक कार्यात्मक भाषा में, टेल कॉल ऑप्टिमाइज़ेशन ऐसा होता है जैसे कि कोई फ़ंक्शन कॉल परिणाम के रूप में आंशिक रूप से मूल्यांकन की गई अभिव्यक्ति को लौटा सकती है, जिसका मूल्यांकन कॉलर द्वारा किया जाएगा।

f x = g x

f 6 जी 6 तक कम हो जाता है। इसलिए यदि कार्यान्वयन जी 6 को परिणाम के रूप में वापस कर सकता है, और फिर उस अभिव्यक्ति को कॉल कर सकता है जो एक स्टैक फ्रेम को बचाएगा।

भी

f x = if c x then g x else h x.

च 6 को या तो जी 6 या एच 6 तक कम करता है। इसलिए यदि कार्यान्वयन सी 6 का मूल्यांकन करता है और पाता है कि यह सत्य है तो इसे कम किया जा सकता है,

if true then g x else h x ---> g x

f x ---> h x

एक साधारण गैर पूंछ कॉल अनुकूलन दुभाषिया इस तरह दिख सकता है,

class simple_expresion
{
    ...
public:
    virtual ximple_value *DoEvaluate() const = 0;
};

class simple_value
{
    ...
};

class simple_function : public simple_expresion
{
    ...
private:
    simple_expresion *m_Function;
    simple_expresion *m_Parameter;

public:
    virtual simple_value *DoEvaluate() const
    {
        vector<simple_expresion *> parameterList;
        parameterList->push_back(m_Parameter);
        return m_Function->Call(parameterList);
    }
};

class simple_if : public simple_function
{
private:
    simple_expresion *m_Condition;
    simple_expresion *m_Positive;
    simple_expresion *m_Negative;

public:
    simple_value *DoEvaluate() const
    {
        if (m_Condition.DoEvaluate()->IsTrue())
        {
            return m_Positive.DoEvaluate();
        }
        else
        {
            return m_Negative.DoEvaluate();
        }
    }
}

एक पूंछ कॉल अनुकूलन दुभाषिया इस तरह लग सकता है,

class tco_expresion
{
    ...
public:
    virtual tco_expresion *DoEvaluate() const = 0;
    virtual bool IsValue()
    {
        return false;
    }
};

class tco_value
{
    ...
public:
    virtual bool IsValue()
    {
        return true;
    }
};

class tco_function : public tco_expresion
{
    ...
private:
    tco_expresion *m_Function;
    tco_expresion *m_Parameter;

public:
    virtual tco_expression *DoEvaluate() const
    {
        vector< tco_expression *> parameterList;
        tco_expression *function = const_cast<SNI_Function *>(this);
        while (!function->IsValue())
        {
            function = function->DoCall(parameterList);
        }
        return function;
    }

    tco_expresion *DoCall(vector<tco_expresion *> &p_ParameterList)
    {
        p_ParameterList.push_back(m_Parameter);
        return m_Function;
    }
};

class tco_if : public tco_function
{
private:
    tco_expresion *m_Condition;
    tco_expresion *m_Positive;
    tco_expresion *m_Negative;

    tco_expresion *DoEvaluate() const
    {
        if (m_Condition.DoEvaluate()->IsTrue())
        {
            return m_Positive;
        }
        else
        {
            return m_Negative;
        }
    }
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.