जब कोई TCO नहीं है, तो स्टैक उड़ाने के बारे में चिंता कब करें?


14

हर बार जब जेवीएम को लक्षित करने वाली एक नई प्रोग्रामिंग भाषा के बारे में चर्चा होती है, तो अनिवार्य रूप से लोग ऐसी बातें कहते हैं:

"जेवीएम टेल-कॉल ऑप्टिमाइज़ेशन का समर्थन नहीं करता है, इसलिए मैं बहुत सारे विस्फोट के पूर्वानुमान की भविष्यवाणी करता हूं"

उस विषय पर हजारों विविधताएं हैं।

अब मुझे पता है कि कुछ भाषा, उदाहरण के लिए क्लोजर, एक विशेष पुनरावृत्ति निर्माण है जिसे आप उपयोग कर सकते हैं।

मुझे समझ में नहीं आता है: पूंछ-कॉल अनुकूलन की कमी कितनी गंभीर है? मुझे इसकी चिंता कब करनी चाहिए?

भ्रम का मेरा मुख्य स्रोत शायद इस तथ्य से आता है कि जावा अब तक की सबसे रसीली भाषाओं में से एक है और जेवीएम भाषाओं में से कुछ काफी अच्छी तरह से कर रही हैं। यदि TCO की कमी वास्तव में किसी चिंता का विषय है तो यह कैसे संभव है?


4
यदि आपके पास TCO के बिना स्टैक को उड़ाने के लिए पर्याप्त गहरी पुनरावृत्ति है तो आपको TCO के साथ भी समस्या होगी
शाफ़्ट फ्रीक

18
@ratchet_freak यह बकवास है। योजना में लूप भी नहीं है, लेकिन क्योंकि TCO समर्थन को निर्दिष्ट करता है, इसलिए डैट्स के एक बड़े सेट पर पुनरावर्ती पुनरावृत्ति अनिवार्य लूप की तुलना में अधिक महंगा नहीं है (बोनस के साथ जो स्कीम एक रिटर्न लौटाता है)।
इसका कर्क रात्रि

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

2
आखिरी बार मैंने देखा, 80x86 (देशी) टेल-कॉल ऑप्टिमाइज़ेशन नहीं करता है। लेकिन इससे भाषा डेवलपर्स को उन भाषाओं को पोर्ट करने से नहीं रोका गया है जो इसका उपयोग करते हैं। संकलक की पहचान तब होती है जब वह एक छलांग बनाम एक jsr का उपयोग कर सकता है, और हर कोई खुश है। आप एक जेवीएम पर एक ही काम कर सकते हैं।
kdgregory

3
@kdgregory: लेकिन x86 में GOTOJVM नहीं है। और x86 को इंटरोप प्लेटफॉर्म के रूप में उपयोग नहीं किया जाता है। JVM के पास GOTOजावा प्लेटफ़ॉर्म चुनने का एक मुख्य कारण इंटरॉप नहीं है। यदि आप JVM पर TCO को लागू करना चाहते हैं, तो आपको स्टैक के लिए कुछ करना होगा। इसे स्वयं प्रबंधित करें (यानी JVM कॉल स्टैक का उपयोग बिल्कुल न करें), trampolines का उपयोग करें, अपवादों का उपयोग करें GOTO, ऐसा कुछ। उन सभी मामलों में, आप JVM कॉल स्टैक के साथ असंगत हो जाते हैं। जावा के साथ स्टैक-संगत होना असंभव है, TCO, और उच्च प्रदर्शन है। आपको उन तीनों में से एक का त्याग करना होगा।
जोर्ग डब्ल्यू मित्तग

जवाबों:


16

इस पर विचार करें, मान लें कि हमने जावा में सभी छोरों से छुटकारा पा लिया (संकलक लेखक हड़ताल या कुछ पर हैं)। अब हम तथ्यात्मक लिखना चाहते हैं, इसलिए हम कुछ इस तरह से सही कर सकते हैं

int factorial(int i){ return factorial(i, 1);}
int factorial(int i, int accum){
  if(i == 0) return accum;
  return factorial(i-1, accum * i);
}

अब हम बहुत चालाक महसूस कर रहे हैं, हम बिना छोरों के भी अपने तथ्य को लिखने में कामयाब रहे हैं! लेकिन जब हम परीक्षण करते हैं, तो हम नोटिस करते हैं कि किसी भी उचित आकार की संख्या के साथ, हमें स्टैकओवरफ़्लो त्रुटियां हो रही हैं क्योंकि कोई टीसीओ नहीं है।

वास्तविक जावा में यह कोई समस्या नहीं है। यदि हमारे पास कभी पूंछ पुनरावर्ती एल्गोरिदम है, तो हम इसे एक लूप में बदल सकते हैं और ठीक हो सकते हैं। हालांकि, बिना छोरों वाली भाषाओं के बारे में क्या? तो फिर तुम सिर्फ hosed कर रहे हैं। इसीलिए क्लोजर का यह recurरूप है, इसके बिना, यह पूरी तरह से ट्यूरिंग नहीं है (अनंत छोरों को करने का कोई तरीका नहीं)।

जेवीएम, फ्रीज, कावा (योजना) को लक्षित करने वाली कार्यात्मक भाषाओं का वर्ग, क्लजोर हमेशा पूंछ कॉल की कमी से निपटने की कोशिश कर रहा है, क्योंकि इन भाषाओं में, टीसी लूप्स करने का मुहावरेदार तरीका है! यदि योजना के लिए अनुवाद किया जाता है, तो ऊपर वाला गुट एक अच्छा तथ्य होगा। यह भयानक रूप से असुविधाजनक होगा यदि 5000 बार लूपिंग करने से आपका प्रोग्राम क्रैश हो गया। यह चारों ओर काम किया जा सकता है, recurविशेष रूपों के साथ , एनोटेशन, जो भी हो, सेल्फ कॉल, ट्रैंपोलिनिंग को अनुकूलित करने के संकेत देता है। लेकिन वे सभी प्रोग्रामर पर प्रदर्शन हिट या अनावश्यक काम करने के लिए मजबूर करते हैं।

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

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

return foo(bar, baz); // foo is just a simple method

या पुनरावृत्ति हैं। हालाँकि, टीसी के वर्ग के लिए जो इसमें फिट नहीं है, हर JVM भाषा को दर्द महसूस होता है।

हालाँकि, एक सभ्य कारण है कि हमारे पास अभी तक TCO नहीं है। JVM हमें स्टैक के निशान देता है। TCO के साथ हम व्यवस्थित रूप से स्टैम्पफ्रेम को समाप्त कर देते हैं जो हमें पता है कि "डूम" हैं, लेकिन JVM वास्तव में स्टैकट्रेस के लिए बाद में ये चाहते हैं! मान लें कि हम एक एफएसएम को इस तरह लागू करते हैं, जहां प्रत्येक राज्य अगले पूंछता है। हम पिछले राज्यों के सभी रिकॉर्ड को मिटा देंगे, इसलिए एक ट्रेसबैक हमें दिखाएगा कि हम किस राज्य में हैं, लेकिन कुछ भी नहीं है कि हम वहां कैसे पहुंचे।

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


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

2
दिलचस्प है, आईबीएम द्वारा J9 JVM TCO प्रदर्शन करता है
जोर्ज डब्ल्यू मित्तग 23

1
@jozefg दिलचस्प बात यह है कि लूप्स के लिए स्टैकट्रेस प्रविष्टियों के बारे में किसी को परवाह नहीं है, इसलिए स्टैकट्रेस तर्क पानी नहीं रखता है, कम से कम पूंछ पुनरावर्ती कार्यों के लिए।
इंगो

2
@MasonWheeler यह बिल्कुल मेरी बात है: स्टैकट्रेस आपको यह नहीं बताता है कि यह किस पुनरावृत्ति में हुआ है। आप इसे केवल अप्रत्यक्ष रूप से देख सकते हैं, लूप वेरिएबल्स आदि का निरीक्षण करके, आप एक पूंछ पुनरावर्ती फ़ंक्शन की कई हंटर स्टैक ट्रेस प्रविष्टियों को क्यों चाहेंगे? केवल अंतिम एक दिलचस्प है! और, छोरों की तरह, आप यह निर्धारित कर सकते हैं कि स्थानीय वैरिएबल, तर्क मूल्यों आदि का निरीक्षण करके यह कौन सी पुनरावृत्ति थी
इंगो

3
@Ingo: यदि कोई फ़ंक्शन केवल स्वयं के साथ पुनरावृत्ति करता है, तो स्टैक ट्रेस अधिक नहीं दिखा सकता है। यदि, हालांकि, फ़ंक्शन का एक समूह पारस्परिक रूप से पुनरावर्ती है, तो एक स्टैक ट्रेस कभी-कभी एक महान सौदा दिखा सकता है।
सुपरकैट

7

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

JVM की सीमाओं के आसपास काम करने के तरीके हैं:

  1. सरल पूंछ पुनरावृत्ति संकलक द्वारा एक लूप के लिए अनुकूलित किया जा सकता है।
  2. यदि कार्यक्रम निरंतर-गुजरने वाली शैली में है, तो "ट्रम्पोलिनिंग" का उपयोग करना तुच्छ है। यहां, एक फ़ंक्शन अंतिम परिणाम नहीं लौटाता है, लेकिन एक निरंतरता जिसे फिर बाहर पर निष्पादित किया जाता है। यह तकनीक एक संकलक लेखक को मनमाने ढंग से जटिल नियंत्रण प्रवाह को मॉडल करने की अनुमति देती है।

इसके लिए एक बड़े उदाहरण की आवश्यकता हो सकती है। क्लोजर वाली भाषा (उदाहरण के लिए जावास्क्रिप्ट या समान) पर विचार करें। हम इस तथ्य को लिख सकते हैं

def fac(n, acc = 1) = if (n <= 1) acc else n * fac(n-1, acc*n)

print fac(x)

अब हम इसके बदले कॉलबैक वापस कर सकते हैं:

def fac(n, acc = 1) =
  if (n <= 1) acc
  else        (() => fac(n-1, acc*n))  // this isn't full CPS, but you get the idea…

var continuation = (() => fac(x))
while (continuation instanceof function) {
  continuation = continuation()
}
var result = continuation
print result

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

इस तकनीक का एक बड़ा नुकसान यह है कि यह डिबग करने के लिए बहुत कठिन है, लागू करने के लिए थोड़ा कठिन है, और कम प्रदर्शन करने वाले - सभी बंद और अप्रत्यक्ष उपयोग कर रहा हूं।

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


1
"जैसा कि TCO स्टैक के एक हिस्से का पुन: उपयोग करता है, एक अपवाद से एक स्टैक ट्रेस अधूरा होगा," - हाँ, लेकिन फिर, एक लूप के भीतर से एक स्टैकट्रेस या तो अपूर्ण है - यह रिकॉर्ड नहीं करता है कि लूप को कितनी बार निष्पादित किया गया था। हालांकि, भले ही जेवीएम उचित टेल कॉल का समर्थन करेगा, एक डिबगिंग के दौरान, फिर भी बाहर निकल सकता है। और फिर, उत्पादन के लिए, TCO को यह सुनिश्चित करने में सक्षम करें कि कोड 100,000 या 100,000,000 पूंछ कॉल के साथ चलता है।
इंगो

1
@Ingo No. (1) जब लूप को पुनरावृत्ति के रूप में लागू नहीं किया जाता है, तो स्टैक पर दिखाने के लिए उनके लिए कोई तर्क नहीं है (पूंछ कॉल ≠ कूद ≠ कॉल)। (2) TCO पूंछ पुनरावृत्ति अनुकूलन की तुलना में अधिक सामान्य है। मेरा जवाब एक उदाहरण के रूप में पुनरावृत्ति का उपयोग करता है । (३) यदि आप एक ऐसी शैली में प्रोग्रामिंग कर रहे हैं जो TCO पर निर्भर है, तो इस अनुकूलन को बंद करना एक विकल्प नहीं है - पूर्ण TCO या पूर्ण स्टैक के निशान या तो एक भाषा सुविधा है, या वे नहीं हैं। ईजी योजना एक अधिक उन्नत अपवाद प्रणाली के साथ TCO कमियों को संतुलित करने का प्रबंधन करती है।
दोपहर

1
(१) पूरी तरह सहमत। लेकिन उसी तर्क से, सैकड़ों और हजारों स्टैक ट्रेस प्रविष्टियों को रखने का कोई औचित्य नहीं है जो सभी return foo(....);विधि foo(2) में पूरी तरह से सहमत हैं, ज़ाहिर है। फिर भी, हम लूप, असाइनमेंट (!), स्टेटमेंट सीक्वेंस से अधूरे ट्रेसिंग को स्वीकार करते हैं। उदाहरण के लिए, यदि आपको एक चर में अप्रत्याशित मूल्य मिलता है, तो आप निश्चित रूप से जानना चाहते हैं कि यह वहां कैसे मिला। लेकिन आप उस मामले में लापता निशान के बारे में शिकायत नहीं करते हैं। क्योंकि यह किसी तरह हमारे दिमाग में उत्कीर्ण है कि ए) यह केवल कॉल बी पर होता है) यह सभी कॉल पर होता है। दोनों को कोई मतलब नहीं है, IMHO।
इंगो

(३) असहमत होना। मैं कोई कारण नहीं देख सकता कि आकार एन की समस्या के साथ मेरे कोड को डिबग करना असंभव क्यों होना चाहिए, कुछ एन छोटे के लिए सामान्य स्टैक के साथ दूर होने के लिए। और फिर, स्विच को चालू करने और TCO को चालू करने के लिए - प्रभावी रूप से प्रोबैम आकार पर कसना छोड़ने।
इंगो

@ इन्नो “असहमत। मैं कोई कारण नहीं देख सकता कि आकार एन की समस्या के साथ मेरे कोड को डिबग करना असंभव होना चाहिए, कुछ एन छोटे के लिए सामान्य स्टैक के साथ दूर होने के लिए। " यदि TCO / TCE एक CPS परिवर्तन के लिए है, तो इसे बंद करने से स्टैक ओवरफ्लो हो जाएगा और प्रोग्राम क्रैश हो जाएगा, इसलिए कोई डीबगिंग संभव नहीं होगा। Google ने V8 JS में TCO को लागू करने से इनकार कर दिया, क्योंकि यह मुद्दा संयोगवश उत्पन्न हुआ था । वे कुछ विशेष वाक्यविन्यास चाहते हैं ताकि प्रोग्रामर घोषित कर सके कि वह वास्तव में TCO और स्टैक ट्रेस का नुकसान चाहता है। क्या किसी को पता है कि अपवाद भी TCO द्वारा खराब किए गए हैं?
शेल्बी मूर III

6

एक कार्यक्रम में कॉल का एक महत्वपूर्ण हिस्सा पूंछ कॉल हैं। हर सबरूटीन में एक आखिरी कॉल होती है, इसलिए हर सबरूटीन में कम से कम एक टेल कॉल होती है। टेल कॉल की प्रदर्शन विशेषताएं हैं, GOTOलेकिन एक सबरूटीन कॉल की सुरक्षा।

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

PTC के बिना, आपको GOTOनियंत्रण प्रवाह या कुछ इस तरह से Trampolines या अपवाद का उपयोग करना होगा । यह बहुत बदसूरत है, और इतना नहीं कि प्रत्यक्ष 1: 1 राज्य मशीन का प्रतिनिधित्व करता है।

(नोट कैसे मैं चतुराई से बोरिंग "लूप" उदाहरण का उपयोग से बचते रहे। यह एक उदाहरण है जहां पीटीसी एक भाषा में भी उपयोगी होते हैं है साथ छोरों।)

मैंने जानबूझकर TCO के बजाय "प्रॉपर टेल कॉल" शब्द का इस्तेमाल किया। TCO एक संकलक अनुकूलन है। PTC एक भाषा विशेषता है जिसमें TCO प्रदर्शन करने के लिए प्रत्येक कंपाइलर की आवश्यकता होती है ।


The vast majority of calls in a program are tail calls. नहीं अगर विधियों का "विशाल बहुमत" कहा जाता है अपने स्वयं के एक से अधिक कॉल करते हैं। Every subroutine has a last call, so every subroutine has at least one tail call. यह तुच्छ रूप से असत्य है return a + b। (जब तक आप कुछ पागल भाषा में न हों जहाँ बुनियादी अंकगणितीय संक्रियाओं को फ़ंक्शन कॉल के रूप में परिभाषित किया जाता है।)
मेसन व्हीलर

1
"दो नंबर जोड़ना दो नंबर जोड़ना है।" उन भाषाओं को छोड़कर जहां यह नहीं है। लिस्प / स्कीम में + ऑपरेशन के बारे में क्या जहां एक एकल अंकगणितीय ऑपरेटर एक मनमानी संख्या ले सकता है? (+ 1 2 3) एक कार्य के रूप में लागू करने का एकमात्र एकमात्र तरीका है।
एविक्टोस

1
@ मेसन व्हीलर: एब्सट्रैक्ट इनवर्शन का क्या मतलब है?
जियोर्जियो

1
@MasonWheeler कि, एक शक के बिना, एक तकनीकी विषय पर सबसे हाथ से लहराती विकिपीडिया प्रविष्टि है जिसे मैंने कभी देखा है। मैंने कुछ संदिग्ध प्रविष्टियाँ देखी हैं, लेकिन यह सिर्फ ... वाह।
एविक्टाटोस

1
@MasonWheeler: क्या आप लिस्प के पेज 22 और 23 पर सूची की लंबाई के कार्यों के बारे में बात कर रहे हैं? टेल-कॉल संस्करण लगभग 1.2x जितना जटिल है, कहीं भी 3x के पास नहीं है। मैं यह भी स्पष्ट नहीं कर रहा हूं कि अमूर्त व्युत्क्रम का क्या मतलब है।
माइकल शॉ

4

"जेवीएम टेल-कॉल ऑप्टिमाइज़ेशन का समर्थन नहीं करता है, इसलिए मैं बहुत सारे विस्फोट के पूर्वानुमान की भविष्यवाणी करता हूं"

जो कोई भी यह कहता है (1) पूंछ-कॉल अनुकूलन को नहीं समझता है, या (2) जेवीएम, या (3) दोनों को नहीं समझता है।

मैं विकिपीडिया से टेल-कॉल की परिभाषा के साथ शुरू करूँगा (यदि आपको विकिपीडिया पसंद नहीं है, तो यहां एक विकल्प है ):

कंप्यूटर विज्ञान में, एक टेल कॉल एक सबरूटीन कॉल है जो कि अंतिम प्रक्रिया के रूप में एक और प्रक्रिया के अंदर होती है; यह एक रिटर्न वैल्यू का उत्पादन कर सकता है जो फिर कॉलिंग प्रक्रिया द्वारा तुरंत लौटाया जाता है

नीचे दिए गए कोड में, कॉल bar()टेल कॉल टू foo():

private void foo() {
    // do something
    bar()
}

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

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

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

टेल कॉल ऑप्टिमाइज़ेशन को लागू करने के लिए, आपको दो चीजों की आवश्यकता है:

  • एक प्लेटफॉर्म जो सबट्रूटीन कॉल के अलावा ब्रांचिंग का समर्थन करता है।
  • एक स्थिर विश्लेषक जो यह निर्धारित कर सकता है कि क्या पूंछ कॉल अनुकूलन संभव है।

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

स्थैतिक विश्लेषण टुकड़ा क्या मुश्किल है। एक ही कार्य के भीतर, यह कोई समस्या नहीं है। उदाहरण के लिए, यहां मानों को योग करने के लिए एक पूंछ-पुनरावर्ती स्काला फ़ंक्शन है List:

def sum(acc:Int, list:List[Int]) : Int = {
  if (list.isEmpty) acc
  else sum(acc + list.head, list.tail)
}

यह कार्य निम्नलिखित बायोटेक में बदल जाता है:

public int sum(int, scala.collection.immutable.List);
  Code:
   0:   aload_2
   1:   invokevirtual   #63; //Method scala/collection/immutable/List.isEmpty:()Z
   4:   ifeq    9
   7:   iload_1
   8:   ireturn
   9:   iload_1
   10:  aload_2
   11:  invokevirtual   #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
   14:  invokestatic    #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
   17:  iadd
   18:  aload_2
   19:  invokevirtual   #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
   22:  checkcast   #59; //class scala/collection/immutable/List
   25:  astore_2
   26:  istore_1
   27:  goto    0

नोट goto 0अंत में। तुलना करके, एक बराबर जावा फ़ंक्शन (जिसे Iteratorस्कैला सूची को सिर और पूंछ में तोड़ने के व्यवहार की नकल करने के लिए उपयोग करना चाहिए ) निम्नलिखित बायोटेक में बदल जाता है। ध्यान दें कि पिछले दो ऑपरेशन अब एक आह्वान हैं , जिसके बाद उस पुनरावर्ती मंगलाचरण द्वारा उत्पादित मूल्य का एक स्पष्ट वापसी होती है।

public static int sum(int, java.util.Iterator);
  Code:
   0:   aload_1
   1:   invokeinterface #64,  1; //InterfaceMethod java/util/Iterator.hasNext:()Z
   6:   ifne    11
   9:   iload_0
   10:  ireturn
   11:  iload_0
   12:  aload_1
   13:  invokeinterface #70,  1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
   18:  checkcast   #25; //class java/lang/Integer
   21:  invokevirtual   #74; //Method java/lang/Integer.intValue:()I
   24:  iadd
   25:  aload_1
   26:  invokestatic    #43; //Method sum:(ILjava/util/Iterator;)I
   29:  ireturn

एकल फ़ंक्शन का टेल कॉल ऑप्टिमाइज़ेशन तुच्छ है: कंपाइलर देख सकता है कि कोई कोड नहीं है जो कॉल के परिणाम का उपयोग करता है, इसलिए यह इनवोक को ए के साथ बदल सकता है goto

जहां जीवन मुश्किल हो जाता है अगर आपके पास कई तरीके हैं। जेवीएम की शाखाओं के निर्देश, सामान्य उद्देश्य प्रोसेसर जैसे कि 80x86 के विपरीत, एक एकल विधि तक ही सीमित हैं। यदि आपके पास निजी विधियां हैं तो यह अभी भी अपेक्षाकृत सरल है: कंपाइलर उन विधियों को उपयुक्त रूप से इनलाइन करने के लिए स्वतंत्र है, इसलिए टेल कॉल को अनुकूलित कर सकते हैं (यदि आप सोच रहे हैं कि यह कैसे काम कर सकता है, तो एक सामान्य विधि पर विचार करें जो switchनियंत्रण व्यवहार का उपयोग करता है )। आप इस तकनीक को एक ही वर्ग में कई सार्वजनिक तरीकों से भी बढ़ा सकते हैं: संकलक विधि निकायों को इनलाइन करता है, सार्वजनिक पुल के तरीके प्रदान करता है, और आंतरिक कॉल कूदता है।

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

जो मुझे आपके प्रश्न के दूसरे भाग में लाता है, जिसे मैं "हमें ध्यान रखना चाहिए?"

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

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

कोड के लिए जो टेल-कॉल अनुकूलन योग्य है, उस कोड को अनुवाद करने योग्य रूप में अनुवाद करना अक्सर सरल होता है। उदाहरण के लिए, उस sum()फ़ंक्शन को जो मैंने पहले दिखाया था, को सामान्यीकृत किया जा सकता है foldLeft()। यदि आप स्रोत को देखते हैं, तो आप देखेंगे कि यह वास्तव में पुनरावृत्त ऑपरेशन के रूप में लागू किया गया है। Jörg W Mittag में फ़ंक्शन कॉल के माध्यम से कार्यान्वित एक राज्य मशीन का एक उदाहरण था; बहुत सारे कुशल (और बनाए रखने योग्य) राज्य मशीन कार्यान्वयन हैं जो फ़ंक्शन कॉल पर भरोसा नहीं करते हैं जो जंप में अनुवादित हो रहे हैं।

मैं पूरी तरह से अलग कुछ के साथ खत्म करेंगे। यदि आप SICP में फुटनोट्स से अपना रास्ता गुगल करते हैं, तो आप यहाँ समाप्त हो सकते हैं । मुझे व्यक्तिगत रूप से लगता है कि मेरे संकलक JSRद्वारा प्रतिस्थापित करने की तुलना में बहुत अधिक दिलचस्प जगह है JUMP


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

@ सुपरकैट - मुझे यकीन नहीं है कि आपका सवाल क्या है। इस पोस्ट का पहला बिंदु यह है कि कंपाइलर यह नहीं जान सकता है कि सभी संभावित कैलेज़ का स्टैक फ्रेम कैसा दिख सकता है (याद रखें कि स्टैक फ्रेम न केवल फ़ंक्शन तर्क बल्कि इसके स्थानीय चर भी रखता है)। मुझे लगता है कि आप एक ओपकोड जोड़ सकते हैं जो संगत फ़्रेम के लिए एक रनटाइम चेक करता है, लेकिन जो मुझे पोस्ट के दूसरे भाग में लाता है: वास्तविक मूल्य क्या है?
kdgregory
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.