बहुत सरलता से, टेल-कॉल ऑप्टिमाइज़ेशन क्या है?
अधिक विशेष रूप से, कुछ छोटे कोड स्निपेट क्या हैं, जहां इसे लागू किया जा सकता है, और क्यों नहीं, स्पष्टीकरण के साथ क्यों?
बहुत सरलता से, टेल-कॉल ऑप्टिमाइज़ेशन क्या है?
अधिक विशेष रूप से, कुछ छोटे कोड स्निपेट क्या हैं, जहां इसे लागू किया जा सकता है, और क्यों नहीं, स्पष्टीकरण के साथ क्यों?
जवाबों:
टेल-कॉल ऑप्टिमाइज़ेशन वह जगह है जहाँ आप किसी फ़ंक्शन के लिए एक नया स्टैक फ्रेम आवंटित करने से बचने में सक्षम होते हैं क्योंकि कॉलिंग फ़ंक्शन बस उस फ़ंक्शन को प्राप्त होने वाले मान को वापस कर देगा। सबसे आम उपयोग पूंछ-पुनरावृत्ति है, जहां पूंछ-कॉल अनुकूलन का लाभ लेने के लिए लिखा गया एक पुनरावर्ती फ़ंक्शन निरंतर स्टैक स्थान का उपयोग कर सकता है।
योजना कुछ प्रोग्रामिंग भाषाओं में से एक है जो इस बात की गारंटी देती है कि किसी भी कार्यान्वयन को यह अनुकूलन प्रदान करना होगा (जावास्क्रिप्ट भी करता है, ईएस 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)। यह गैर-पूंछ-पुनरावर्ती तथ्य के साथ ऐसा नहीं है, और ऐसे बड़े मूल्यों के कारण स्टैक ओवरफ्लो हो सकता है।
आइए एक सरल उदाहरण के माध्यम से चलते हैं: सी में कार्यान्वित तथ्यात्मक फ़ंक्शन।
हम स्पष्ट पुनरावर्ती परिभाषा के साथ शुरू करते हैं
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;
}
जैसा कि हम यहां देख सकते हैं, एक पर्याप्त रूप से उन्नत ऑप्टिमाइज़र पूंछ-पुनरावृत्ति को पुनरावृत्ति के साथ बदल सकता है, जो कि कहीं अधिक कुशल है क्योंकि आप फ़ंक्शन कॉल ओवरहेड से बचते हैं और केवल स्टैक स्पेस की एक निरंतर मात्रा का उपयोग करते हैं।
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)
ऐसा इसलिए है क्योंकि इनमें से किसी भी फ़ंक्शन में होने वाली अंतिम बात किसी अन्य फ़ंक्शन को कॉल करना है।
संभवतः पूंछ कॉल, पुनरावर्ती पूंछ कॉल और पूंछ कॉल अनुकूलन के लिए मुझे मिला सबसे अच्छा उच्च स्तरीय विवरण ब्लॉग पोस्ट है
डैन सुगल्स्की द्वारा। पूंछ कॉल अनुकूलन पर वह लिखते हैं:
एक पल के लिए, इस सरल कार्य पर विचार करें:
sub foo (int a) { a += 15; return bar(a); }
तो, आप या आपकी भाषा संकलक क्या कर सकते हैं, करते हैं? खैर, यह क्या कर सकता है फार्म
return somefunc();
के निचले स्तर के अनुक्रम में बारी हैpop stack frame; goto somefunc();
। हमारे उदाहरण में, इसका मतलब है कि हम कॉल करने से पहलेbar
,foo
खुद को साफ करते हैं और फिर,bar
सबरूटीन के रूप में कॉल करने के बजाय , हमgoto
शुरू करने के लिए एक निम्न-स्तरीय ऑपरेशन करते हैंbar
।Foo
यह पहले से ही स्टैक से खुद को साफ कर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; }
इस विवरण के बारे में मुझे क्या पसंद है, यह अनिवार्य भाषा की पृष्ठभूमि (सी, सी ++, जावा) से आने वाले लोगों के लिए कितना आसान और आसान है
foo
कार्य पूंछ कॉल अनुकूलित नहीं है? यह केवल एक फ़ंक्शन को अपने अंतिम चरण के रूप में बुला रहा है, और यह केवल उस मूल्य को वापस कर रहा है, है ना?
सबसे पहले ध्यान दें कि सभी भाषाएं इसका समर्थन नहीं करती हैं।
TCO पुनरावृत्ति के एक विशेष मामले पर लागू होता है। इसका सार यह है, यदि किसी कार्य में आप जो अंतिम कार्य करते हैं, वह स्वयं कॉल होता है (जैसे कि यह "टेल" स्थिति से स्वयं को कॉल कर रहा है), इसे कंपाइलर द्वारा मानक पुनरावृत्ति के बजाय पुनरावृत्ति की तरह कार्य करने के लिए अनुकूलित किया जा सकता है।
आप देखते हैं, आम तौर पर पुनरावृत्ति के दौरान, रनटाइम को सभी पुनरावर्ती कॉल का ट्रैक रखने की आवश्यकता होती है, ताकि जब कोई वापस आए तो वह पिछली कॉल पर फिर से शुरू कर सके। (यह कैसे काम करता है की एक दृश्य विचार प्राप्त करने के लिए एक पुनरावर्ती कॉल के परिणाम को मैन्युअल रूप से लिखने का प्रयास करें।) सभी कॉलों का ट्रैक रखने में जगह लेता है, जो तब महत्वपूर्ण हो जाता है जब फ़ंक्शन खुद को बहुत कुछ कहता है। लेकिन TCO के साथ, यह केवल यह कह सकता है कि "शुरुआत में वापस जाएं, केवल इस बार इन नए लोगों के लिए पैरामीटर मान बदलें"। ऐसा कर सकते हैं क्योंकि पुनरावर्ती कॉल के बाद कुछ भी उन मूल्यों को संदर्भित नहीं करता है।
foo
विधि पूंछ कॉल अनुकूलित नहीं है?
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 में परीक्षण किया गया।
इधर देखो:
http://tratt.net/laurie/tech_articles/articles/tail_call_optimization
जैसा कि आप शायद जानते हैं, पुनरावर्ती फ़ंक्शन कॉल एक स्टैक पर कहर बरपा सकते हैं; स्टैक स्पेस से बाहर निकलना आसान है। टेल कॉल ऑप्टिमाइज़ेशन वह तरीका है जिसके द्वारा आप एक पुनरावर्ती शैली एल्गोरिदम बना सकते हैं जो निरंतर स्टैक स्पेस का उपयोग करता है, इसलिए यह बढ़ता नहीं है और बढ़ता है और आपको स्टैक त्रुटियां मिलती हैं।
हमें यह सुनिश्चित करना चाहिए कि फंक्शन में कोई गोटो स्टेटमेंट्स न हों .. फंक्शन कॉल द्वारा इस बात का ध्यान रखा जाए कि कैली फंक्शन में आखिरी चीज हो।
बड़े पैमाने पर पुनरावर्ती अनुकूलन के लिए इसका उपयोग कर सकते हैं, लेकिन छोटे पैमाने पर, फ़ंक्शन कॉल को टेल कॉल करने के लिए निर्देश ओवरहेड वास्तविक उद्देश्य को कम करता है।
TCO के कारण हमेशा चलने वाला कार्य हो सकता है:
void eternity()
{
eternity();
}
पुनरावर्ती फ़ंक्शन दृष्टिकोण में एक समस्या है। यह आकार O (n) के एक कॉल स्टैक का निर्माण करता है, जो हमारी कुल मेमोरी लागत O (n) बनाता है। यह एक स्टैक ओवरफ्लो त्रुटि के प्रति संवेदनशील बनाता है, जहां कॉल स्टैक बहुत बड़ा हो जाता है और अंतरिक्ष से बाहर चला जाता है।
टेल कॉल ऑप्टिमाइज़ेशन (TCO) स्कीम। जहां यह एक लंबे कॉल स्टैक के निर्माण से बचने के लिए पुनरावर्ती कार्यों को अनुकूलित कर सकता है और इसलिए स्मृति लागत को बचाता है।
कई भाषाएँ हैं जो TCO कर रही हैं जैसे (जावास्क्रिप्ट, रूबी और कुछ C) जबकि पायथन और जावा TCO नहीं करते हैं।
जावास्क्रिप्ट भाषा ने :) http://2ality.com/2015/06/tail-call-optimization.html का उपयोग करके पुष्टि की है
एक कार्यात्मक भाषा में, टेल कॉल ऑप्टिमाइज़ेशन ऐसा होता है जैसे कि कोई फ़ंक्शन कॉल परिणाम के रूप में आंशिक रूप से मूल्यांकन की गई अभिव्यक्ति को लौटा सकती है, जिसका मूल्यांकन कॉलर द्वारा किया जाएगा।
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;
}
}
}