जीएचसी हास्केल में ज्ञापन स्वचालित कब है?


106

मैं यह पता नहीं लगा सकता कि एम 1 जाहिरा तौर पर क्यों यादगार है जबकि एम 2 निम्नलिखित में नहीं है:

m1      = ((filter odd [1..]) !!)

m2 n    = ((filter odd [1..]) !! n)

m1 10000000 पहली कॉल पर लगभग 1.5 सेकंड लेता है, और इसके बाद के कॉल पर इसका एक अंश (संभवतः यह सूची को कैश करता है), जबकि m2 10000000 हमेशा एक ही राशि लेता है (प्रत्येक कॉल के साथ सूची का पुनर्निर्माण)। किसी भी विचार क्या चल रहा है? क्या अंगूठे के कोई नियम हैं जैसे कि और जब जीएचसी एक समारोह को याद करेगा? धन्यवाद।

जवाबों:


112

जीएचसी कार्यों को याद नहीं करता है।

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

m1' = (!!) (filter odd [1..])              -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n

(नोट: हास्केल 98 रिपोर्ट वास्तव (a %)में इसके समान ही एक बाएं ऑपरेटर सेक्शन का वर्णन करती है \b -> (%) a b, लेकिन GHC इसे बताता है (%) a। ये तकनीकी रूप से अलग हैं क्योंकि वे इससे अलग हो सकते हैं seq। मुझे लगता है कि मैंने इस बारे में GHC Trac टिकट जमा किया होगा।)

इसे देखते हुए, आप देख सकते हैं कि m1', अभिव्यक्ति filter odd [1..]किसी भी लैम्ब्डा-एक्सप्रेशन में समाहित नहीं है, इसलिए यह केवल आपके प्रोग्राम के चलने के दौरान एक बार गणना की जाएगी , जबकि m2', filter odd [1..]लैम्बडा-एक्सप्रेशन के दर्ज होने पर, हर बार, यानी के प्रत्येक आह्वान पर m2'। जो आपके द्वारा देखे जा रहे समय के अंतर को स्पष्ट करता है।


वास्तव में, जीएचसी के कुछ संस्करण, कुछ अनुकूलन विकल्पों के साथ, उपरोक्त विवरण से अधिक मूल्य साझा करेंगे। यह कुछ स्थितियों में समस्याग्रस्त हो सकता है। उदाहरण के लिए, फ़ंक्शन पर विचार करें

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])

जीएचसी ध्यान दे सकता है कि yइस पर निर्भर नहीं करता है xऔर फ़ंक्शन को फिर से लिखता है

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])

इस मामले में, नया संस्करण बहुत कम कुशल है क्योंकि इसे मेमोरी से लगभग 1 जीबी पढ़ना होगा जहां yसंग्रहीत किया जाता है, जबकि मूल संस्करण निरंतर स्थान पर चलेगा और प्रोसेसर के कैश में फिट होगा। वास्तव में, जीएचसी 6.12.1 के तहत, फंक्शन fलगभग दोगुनी तेजी से होता है जब अनुकूलन के बिना इसे संकलित किया जाता है -O2


1
मूल्यांकन करने की लागत (फ़िल्टर विषम [1 ..]) वैसे भी शून्य के करीब है - यह आलसी सूची है सब के बाद, इसलिए वास्तविक लागत (x !! 10000000) अनुप्रयोग में है जब सूची वास्तव में मूल्यांकन की जाती है। इसके अलावा, m1 और m2 दोनों का मूल्यांकन केवल एक बार -O2 और -O1 (मेरे ghc 6.12.3 पर) के साथ कम से कम निम्नलिखित परीक्षण के भीतर किया गया लगता है: (परीक्षण = m1 10000000 seqm1 10000000)। जब कोई अनुकूलन ध्वज निर्दिष्ट नहीं किया जाता है तो एक अंतर होता है। और आपके "f" के दोनों वेरिएंट्स में अनुकूलन की परवाह किए बिना 5356 बाइट्स की अधिकतम रेजिडेंसी है, जिस तरह से (जब कुल आवंटन कम होता है -O2 का उपयोग किया जाता है)।
एडका

1
@ Ed'ka: की उपरोक्त परिभाषा के साथ इस परीक्षण कार्यक्रम का प्रयास करें, f: main = interact $ unlines . (show . map f . read) . lines; के साथ या बिना संकलन -O2; तब echo 1 | ./main। यदि आप एक परीक्षण लिखते हैं main = print (f 5), तो yकचरा एकत्र किया जा सकता है क्योंकि इसका उपयोग किया जाता है और दोनों के बीच कोई अंतर नहीं है f
बार्टन

एर, map (show . f . read)निश्चित रूप से होना चाहिए । और अब जब मैंने GHC 6.12.3 डाउनलोड कर लिया है, तो मुझे GHC 6.12.1 के समान परिणाम दिखाई दे रहे हैं। और हां, आप जीएचसी के मूल m1और m2: संस्करणों के बारे में सही हैं जो सक्षम किए गए अनुकूलन के साथ इस प्रकार का प्रदर्शन m2करते हैं m1
बार्टन

हां, अब मैं अंतर देखता हूं (-ओ 2 निश्चित रूप से धीमा है)। इस उदाहरण के लिए धन्यवाद!
एडका

29

m1 की गणना केवल एक बार की जाती है क्योंकि यह एक निरंतर अनुप्रयोग रूप है, जबकि m2 एक CAF नहीं है, और इसलिए प्रत्येक मूल्यांकन के लिए गणना की जाती है।

CAFs पर GHC विकी देखें: http://www.haskell.org/haskellwiki/Cststant_apprative_form


1
स्पष्टीकरण "एम 1 की गणना केवल एक बार की जाती है क्योंकि यह एक निरंतर आवेदक रूप है" मुझे इससे कोई मतलब नहीं है। क्योंकि संभवतः एम 1 और एम 2 दोनों शीर्ष-स्तरीय चर हैं, मुझे लगता है कि इन कार्यों को केवल एक बार गणना की जाती है, चाहे वे सीएएफ हों या नहीं। अंतर यह है कि क्या सूची [1 ..]की गणना केवल एक कार्यक्रम के निष्पादन के दौरान की जाती है या यह फ़ंक्शन के आवेदन के अनुसार एक बार गणना की जाती है, लेकिन क्या यह सीएएफ से संबंधित है?
त्सुयोशी इतो

1
लिंक किए गए पृष्ठ से: "A CAF ... या तो ग्राफ़ के एक टुकड़े पर संकलित किया जा सकता है जो सभी उपयोगों या कुछ साझा कोड द्वारा साझा किया जाएगा जो पहली बार मूल्यांकन किए जाने पर कुछ ग्राफ़ के साथ स्वयं को अधिलेखित कर देगा"। चूंकि m1सीएएफ है, दूसरा लागू होता है और filter odd [1..](सिर्फ नहीं [1..]!) की गणना केवल एक बार की जाती है। GHC भी ध्यान दें सकता है कि m2को संदर्भित करता है filter odd [1..], और एक ही में इस्तेमाल किया thunk के लिए एक लिंक जगह m1है, लेकिन यह एक बुरा विचार होगा: यह कुछ स्थितियों में बड़ी मेमोरी लीक हो सकता है।
एलेक्सी रोमानोव

@Alexey: के बारे में सुधार के लिए धन्यवाद [1..]और filter odd [1..]। बाकी के लिए, मैं अभी भी असंबद्ध हूं। अगर मैं गलत नहीं हूँ, सीएएफ केवल प्रासंगिक है जब हम लोगों का तर्क है कि एक संकलक चाहते है सकता है की जगह filter odd [1..]में m2एक वैश्विक thunk (जो भी में इस्तेमाल एक के रूप में एक ही thunk हो सकता है द्वारा m1)। लेकिन प्रश्नकर्ता की स्थिति में, कंपाइलर ने "अनुकूलन" नहीं किया और मैं इस प्रश्न की प्रासंगिकता नहीं देख सकता।
त्सुयोशी इतो

2
यह प्रासंगिक है कि यह इसमें जगह ले सकता है m1 , और यह करता है।
एलेक्सी रोमानोव

13

दो रूपों के बीच एक महत्वपूर्ण अंतर है: मोनोमोर्फिज्म प्रतिबंध एम 1 पर लागू होता है, लेकिन एम 2 नहीं, क्योंकि एम 2 ने स्पष्ट रूप से तर्क दिए हैं। तो एम 2 का प्रकार सामान्य है लेकिन एम 1 का विशिष्ट है। उन्हें असाइन किए गए प्रकार हैं:

m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a

अधिकांश हास्केल संकलक और व्याख्याकार (उनमें से सभी जो मुझे वास्तव में पता हैं) पॉलिमॉर्फिक संरचनाओं को याद नहीं करते हैं, इसलिए एम 2 की आंतरिक सूची को हर बार इसे फिर से बनाया जाता है, जहां एम 1 का नहीं है।


1
GHCi में इनके साथ खेलने पर ऐसा लगता है कि यह लेट-फ़्लोटिंग ट्रांसफ़ॉर्मेशन (GHC के ऑप्टिमाइज़ेशन पास जो GHCi में उपयोग नहीं किया गया है) पर भी निर्भर करता है। और निश्चित रूप से इन सरल कार्यों को संकलित करते समय, ऑप्टिमाइज़र उन्हें वैसे भी व्यवहारिक रूप से व्यवहार करने में सक्षम होता है (कुछ मानदंड परीक्षणों के अनुसार, मैं किसी भी तरह से अलग मॉड्यूल में फ़ंक्शन के साथ और NOINLINE pragmas के साथ चिह्नित)। संभवत: ऐसा इसलिए है क्योंकि सूची निर्माण और अनुक्रमण किसी भी तरह एक सुपर तंग पाश में जुड़े हुए हैं।
मकुस

1

मुझे यकीन नहीं है, क्योंकि मैं खुद हास्केल के लिए काफी नया हूं, लेकिन ऐसा प्रतीत होता है कि यह मुस्कुराता है दूसरा फ़ंक्शन पैराट्राइज्ड है और पहले वाला नहीं है। फ़ंक्शन की प्रकृति यह है कि, यह परिणाम इनपुट मूल्य पर निर्भर करता है और कार्यात्मक प्रतिमान में यह केवल इनपुट पर निर्भर करता है। स्पष्ट निहितार्थ यह है कि बिना किसी मापदण्ड के कोई फ़ंक्शन हमेशा एक ही मान देता है, चाहे वह कोई भी हो।

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

primes = filter isPrime [2..]
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
        where f `divides` n = (n `mod` f) == 0

फिर इसका परीक्षण करने के लिए, मैंने GHCI में प्रवेश किया और लिखा primes !! 1000:। इसमें कुछ सेकंड लगे, लेकिन आखिरकार मुझे जवाब मिल गया 7927:। फिर मैंने फोन किया primes !! 1001और तुरंत जवाब मिला। इसी तरह एक पल में मुझे इसका परिणाम मिल गया take 1000 primes, क्योंकि हास्केल को 1001 वें तत्व को वापस करने के लिए पूरी हजार-तत्व सूची की गणना करनी थी।

इस प्रकार यदि आप अपने कार्य को ऐसे लिख सकते हैं कि इसमें कोई पैरामीटर नहीं है, तो आप शायद यह चाहते हैं। ;)

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