algorithm संरचनात्मक - पूंछ कॉल अनुकूलन क्या है?




के प्रकार (7)

  1. हमें यह सुनिश्चित करना चाहिए कि फ़ंक्शन में कोई गेटो स्टेटमेंट न हो .. कैलिफ़ फ़ंक्शन में अंतिम कॉल होने पर फ़ंक्शन कॉल द्वारा देखभाल की गई।

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

  3. टीसीओ हमेशा के लिए चल रहे समारोह का कारण बन सकता है:

    void eternity()
    {
        eternity();
    }
    

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


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

योजना उन कुछ प्रोग्रामिंग भाषाओं में से एक है जो इस कल्पना में गारंटी देते हैं कि किसी भी कार्यान्वयन को इस अनुकूलन को प्रदान करना होगा (जावास्क्रिप्ट भी एक बार ईएस 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)। गैर-पूंछ-पुनरावर्ती तथ्य के साथ यह मामला नहीं है, और ऐसे बड़े मूल्यों के कारण एक ढेर अतिप्रवाह हो सकता है।


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

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

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

एक पल के लिए, इस सरल समारोह पर विचार करें:

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

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

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


टीसीओ (टेल कॉल ऑप्टिमाइज़ेशन) वह प्रक्रिया है जिसके द्वारा एक स्मार्ट कंपाइलर फ़ंक्शन पर कॉल कर सकता है और कोई अतिरिक्त स्टैक स्पेस नहीं ले सकता है। एकमात्र स्थिति जिसमें ऐसा होता है वह यह है कि यदि फ़ंक्शन 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)

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


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

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

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;
}

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


इधर देखो:

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

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


I am the author of a 2048 controller that scores better than any other program mentioned in this thread. An efficient implementation of the controller is available on github.com/aszczepanski/2048 . In a separate repo there is also the code used for training the controller's state evaluation function. The training method is described in the paper .

The controller uses expectimax search with a state evaluation function learned from scratch (without human 2048 expertise) by a variant of temporal difference learning (a reinforcement learning technique). The state-value function uses an n-tuple network , which is basically a weighted linear function of patterns observed on the board. It involved more than 1 billion weights , in total.

प्रदर्शन

At 1 moves/s: 609104 (100 games average)

At 10 moves/s: 589355 (300 games average)

At 3-ply (ca. 1500 moves/s): 511759 (1000 games average)

The tile statistics for 10 moves/s are as follows:

2048: 100%
4096: 100%
8192: 100%
16384: 97%
32768: 64%
32768,16384,8192,4096: 10%

(The last line means having the given tiles at the same time on the board).

For 3-ply:

2048: 100%
4096: 100%
8192: 100%
16384: 96%
32768: 54%
32768,16384,8192,4096: 8%

However, I have never observed it obtaining the 65536 tile.





algorithm recursion language-agnostic tail-recursion tail-call-optimization