क्या हास्केल में पूंछ-पुनरावर्ती अनुकूलन है?


89

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

मैंने निम्नलिखित कार्य लिखे:

--tail recursive
fac :: (Integral a) => a -> a
fac x = fac' x 1 where
    fac' 1 y = y
    fac' x y = fac' (x-1) (x*y) 

--normal recursive
facSlow :: (Integral a) => a -> a
facSlow 1 = 1
facSlow x = x * facSlow (x-1)

ये ध्यान में रखते हुए मान्य हैं कि वे पूरी तरह से इस परियोजना के साथ उपयोग के लिए थे, इसलिए मैंने शून्य या नकारात्मक संख्या की जांच करने की जहमत नहीं उठाई।

हालाँकि, प्रत्येक के लिए एक मुख्य विधि लिखने, उन्हें संकलित करने और उन्हें "समय" कमांड के साथ चलाने पर, दोनों में सामान्य पुनरावर्ती कार्य के साथ समान पुनरावृत्ति होती थी, जिससे पूंछ पुनरावर्ती एक निकलती थी। यह लिस्प में पूंछ-पुनरावर्ती अनुकूलन के संबंध में मैंने जो सुना है, उसके विपरीत है। इसका क्या कारण है?


8
मेरा मानना ​​है कि TCO कुछ कॉल स्टैक को बचाने के लिए एक अनुकूलन है, इसका मतलब यह नहीं है कि आप कुछ सीपीयू समय बचा लेंगे। गलत होने पर मुझे सुधारो।
जेरोम

3
लिस्प के साथ इसका परीक्षण नहीं किया गया है, लेकिन मैंने जो ट्यूटोरियल पढ़ा वह निहित है कि स्टैक की स्थापना अधिक प्रोसेसर लागत को अपने आप में करता है, जबकि संकलित-से-पुनरावृत्त पूंछ-पुनरावर्ती समाधान ने ऐसा करने में कोई भी ऊर्जा (समय) खर्च नहीं किया। अधिक कुशल था।
haskell rascal

1
@ अच्छी तरह से यह बहुत सी चीजों पर निर्भर करता है, लेकिन आमतौर पर कैश भी खेलने में आता है, इसलिए TCO आमतौर पर एक तेज प्रोग्राम भी
बनाएगा

इसका क्या कारण है? एक शब्द में: आलस्य।
डैन बर्टन

दिलचस्प बात facयह है कि आपका अधिक-या-कम है कि कैसे ghc product [n,n-1..1]एक ऑक्ज़िलरी फ़ंक्शन का उपयोग करके गणना करता है prod, लेकिन निश्चित रूप product [1..n]से सरल होगा। मैं केवल यह मान सकता हूं कि उन्होंने इसे अपने दूसरे तर्क में इस आधार पर सख्त नहीं बनाया है कि यह एक प्रकार की चीज है ghc बहुत विश्वास है कि यह एक साधारण संचायक को संकलित कर सकता है।
एंड्रयूसी

जवाबों:


168

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

आइए देखें कि हम facSlow 5एक केस स्टडी के रूप में कैसे मूल्यांकन करते हैं:

facSlow 5
5 * facSlow 4            -- Note that the `5-1` only got evaluated to 4
5 * (4 * facSlow 3)       -- because it has to be checked against 1 to see
5 * (4 * (3 * facSlow 2))  -- which definition of `facSlow` to apply.
5 * (4 * (3 * (2 * facSlow 1)))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120

इसलिए जैसा कि आप चिंतित थे, किसी भी गणना के होने से पहले हमारे पास संख्याओं का निर्माण होता है, लेकिन विपरीत आप चिंतित हैं, वहाँ का कोई ढेर है facSlowसमाप्त करने के लिए इंतजार कर के आसपास फांसी समारोह कॉल - प्रत्येक कमी लागू किया जाता है और चला जाता है, एक छोड़ने स्टैक फ्रेम में अपनी वेक (क्योंकि (*)यह सख्त है और इसलिए इसके दूसरे तर्क के मूल्यांकन को ट्रिगर करता है)।

हास्केल के पुनरावर्ती कार्यों का पुनरावर्ती तरीके से मूल्यांकन नहीं किया जाता है! चारों ओर लटकी हुई कॉलों का एकमात्र ढेर कई गुना है। अगर (*)इसे सख्त डेटा कंस्ट्रक्टर के रूप में देखा जाता है, तो इसे पहरेदार पुनर्संरचना के रूप में जाना जाता है (हालांकि इसे आमतौर पर गैर- स्थैतिक डेटा कंस्ट्रक्टर के साथ संदर्भित किया जाता है , जहां इसके मद्देनजर क्या बचा है - डेटा कंस्ट्रक्टर हैं - जब आगे एक्सेस द्वारा मजबूर किया जाता है)।

अब आइए पूँछ-पुनरावर्ती को देखें fac 5:

fac 5
fac' 5 1
fac' 4 {5*1}       -- Note that the `5-1` only got evaluated to 4
fac' 3 {4*{5*1}}    -- because it has to be checked against 1 to see
fac' 2 {3*{4*{5*1}}} -- which definition of `fac'` to apply.
fac' 1 {2*{3*{4*{5*1}}}}
{2*{3*{4*{5*1}}}}        -- the thunk "{...}" 
(2*{3*{4*{5*1}}})        -- is retraced 
(2*(3*{4*{5*1}}))        -- to create
(2*(3*(4*{5*1})))        -- the computation
(2*(3*(4*(5*1))))        -- on the stack
(2*(3*(4*5)))
(2*(3*20))
(2*60)
120

तो आप देख सकते हैं कि कैसे पूंछ की पुनरावृत्ति ने आपको किसी भी समय या स्थान को बचाया नहीं है। इतना ही नहीं यह समग्र रूप से अधिक कदम उठाता हैfacSlow 5 , बल्कि यह एक नेस्टेड थंक (जैसा कि यहां दिखाया गया है {...}) बनाता है - इसके लिए एक अतिरिक्त स्थान की आवश्यकता होती है - जो भविष्य की गणना, प्रदर्शन किए जाने वाले नेस्टेड गुणा का वर्णन करता है।

इस थंक को तब नीचे की ओर लाकर , स्टैक पर अभिकलन को फिर से जोड़कर, हटा दिया जाता है। दोनों संस्करणों के लिए बहुत लंबी गणनाओं के साथ स्टैक ओवरफ्लो पैदा करने का एक खतरा यहां भी है।

अगर हम इसे हाथ से ऑप्टिमाइज़ करना चाहते हैं, तो हमें बस इतना ही करना है। आप सख्त एप्लिकेशन ऑपरेटर का उपयोग कर सकते हैं$! परिभाषित करने के का हैं

facSlim :: (Integral a) => a -> a
facSlim x = facS' x 1 where
    facS' 1 y = y
    facS' x y = facS' (x-1) $! (x*y) 

यह बल facS' अपने दूसरे तर्क में सख्त होने के लिए करता है। (यह अपने पहले तर्क में पहले से ही सख्त है क्योंकि यह तय करने के लिए मूल्यांकन किया जाना है कि किस परिभाषा facS'को लागू करना है।)

कभी-कभी सख्ती बहुत मदद कर सकती है, कभी-कभी यह एक बड़ी गलती है क्योंकि आलस्य अधिक कुशल है। यहाँ यह एक अच्छा विचार है:

facSlim 5
facS' 5 1
facS' 4 5 
facS' 3 20
facS' 2 60
facS' 1 120
120

मुझे लगता है कि आप क्या हासिल करना चाहते थे।

सारांश

  • यदि आप अपने कोड को ऑप्टिमाइज़ करना चाहते हैं, तो चरण एक को संकलित करना है -O2
  • जब कोई थंक बिल्ड-अप नहीं होता है, तो टेल रीसर्शन केवल अच्छा होता है, और सख्ती जोड़ने से आमतौर पर इसे रोकने में मदद मिलती है, अगर और जहां उपयुक्त हो। ऐसा तब होता है जब आप एक परिणाम का निर्माण कर रहे होते हैं जिसकी आवश्यकता बाद में एक साथ होती है।
  • कभी-कभी पूंछ पुनरावृत्ति एक बुरी योजना है और पहरा पुनरावृत्ति एक बेहतर फिट है, यानी जब आप निर्माण कर रहे हैं तो बिट्स द्वारा बिट्स की आवश्यकता होगी। देखें इस सवाल के बारे में foldrऔरfoldl उदाहरण के लिए, और उन्हें एक दूसरे के खिलाफ परीक्षण करें।

इन दोनों का प्रयास करें:

length $ foldl1 (++) $ replicate 1000 
    "The size of intermediate expressions is more important than tail recursion."
length $ foldr1 (++) $ replicate 1000 
    "The number of reductions performed is more important than tail recursion!!!"

foldl1पूंछ पुनरावर्ती है, जबकि foldr1संरक्षित पुनरावर्तन करता है ताकि आगे की प्रक्रिया / पहुंच के लिए पहले आइटम को तुरंत प्रस्तुत किया जाए। (एक बार बाईं ओर पहला "कोष्ठक" (...((s+s)+s)+...)+s, इसकी इनपुट सूची को पूरी तरह से समाप्त करने के लिए मजबूर करना और भविष्य के संगणना का एक बड़ा हिस्सा बनाने की तुलना में इसके पूर्ण परिणाम की आवश्यकता है जितनी जल्दी हो सके; दूसरा कोष्ठक धीरे-धीरे दाईं ओर s+(s+(...+(s+s)...))इनपुट का उपभोग करता है; बिट को थोड़ा सा सूचीबद्ध करें, इसलिए पूरी चीज अनुकूलन के साथ निरंतर स्थान में संचालित करने में सक्षम है)।

आपको उस हार्डवेयर के आधार पर शून्य की संख्या को समायोजित करने की आवश्यकता हो सकती है जो आप उपयोग कर रहे हैं।


1
@WillNess यह उत्कृष्ट है, धन्यवाद। वापस लेने की कोई आवश्यकता नहीं है। मुझे लगता है कि यह अब पोस्टेरिटी के लिए एक बेहतर जवाब है।
13

4
यह बहुत अच्छा है, लेकिन क्या मैं सख्ती से विश्लेषण के लिए एक सुझाव दे सकता हूं ? मुझे लगता है कि लगभग निश्चित रूप से हाल ही में जीएचसी के किसी भी संस्करण में पूंछ-पुनरावर्ती तथ्य के लिए काम करेंगे।
dfeuer

16

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

{-# LANGUAGE BangPatterns #-}

fac :: (Integral a) => a -> a
fac x = fac' x 1 where
  fac' 1  y = y
  fac' x !y = fac' (x-1) (x*y)

आप उपयोग कर संकलन हैं -O2(या बस -O) GHC शायद इस अपने आप ही में क्या करेंगे कठोरता विश्लेषण चरण।


4
मुझे लगता है कि यह साथ की $!तुलना में स्पष्ट है BangPatterns, लेकिन यह एक अच्छा जवाब है। विशेष रूप से कठोरता विश्लेषण का उल्लेख।
सिंगापोलिमा

7

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

(और हास्केल सीखने में, बाकी के विकी पृष्ठ बहुत बढ़िया हैं!)


0

यदि मुझे सही तरीके से याद है, तो जीएचसी स्वचालित रूप से पूंछ-पुनरावर्ती अनुकूलित वाले सादे पुनरावर्ती कार्यों का अनुकूलन करता है।

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