हास्केल कार्यक्रम के प्रदर्शन का विश्लेषण करने के लिए उपकरण


104

हास्केल सीखने के लिए कुछ प्रोजेक्ट यूलर प्रॉब्लम्स को हल करते समय (इसलिए वर्तमान में मैं पूरी तरह से शुरुआत कर रहा हूं) मैं प्रॉब्लम 12 से अधिक आया । मैंने यह (भोला) समाधान लिखा है:

--Get Number of Divisors of n
numDivs :: Integer -> Integer
numDivs n = toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2

--Generate a List of Triangular Values
triaList :: [Integer]
triaList =  [foldr (+) 0 [1..n] | n <- [1..]]

--The same recursive
triaList2 = go 0 1
  where go cs n = (cs+n):go (cs+n) (n+1)

--Finds the first triangular Value with more than n Divisors
sol :: Integer -> Integer
sol n = head $ filter (\x -> numDivs(x)>n) triaList2

यह समाधान n=500 (sol 500)बेहद धीमा है (अब 2 घंटे से अधिक समय तक चलने वाला), इसलिए मैंने सोचा कि कैसे पता लगाया जाए कि यह समाधान इतना धीमा क्यों है। क्या कोई आज्ञा है जो मुझे बताती है कि अधिकांश संगणना-समय कहाँ बिताया जाता है, इसलिए मुझे पता है कि मेरे हैस्केल-प्रोग्राम का कौन सा हिस्सा धीमा है? एक साधारण प्रोफाइलर की तरह कुछ।

इसे स्पष्ट करने के लिए, मैं तेजी से समाधान के लिए नहीं कह रहा हूं, लेकिन इस समाधान को खोजने का एक तरीका है। यदि आपके पास कोई हैकेल ज्ञान नहीं होगा तो आप कैसे शुरू करेंगे?

मैंने दो triaListकार्यों को लिखने की कोशिश की, लेकिन परीक्षण करने का कोई तरीका नहीं मिला कि कौन सा तेज है, इसलिए यह वह जगह है जहां मेरी समस्याएं शुरू होती हैं।

धन्यवाद

जवाबों:


187

कैसे पता करें कि यह समाधान इतना धीमा क्यों है। क्या ऐसी कोई आज्ञा है जो मुझे बताती है कि अधिकांश संगणना-समय कहाँ व्यतीत होता है इसलिए मुझे पता है कि मेरे हैस्केल-प्रोग्राम का कौन सा भाग धीमा है?

यकीनन! GHC कई उत्कृष्ट उपकरण प्रदान करता है, जिनमें शामिल हैं:

समय और स्थान की रूपरेखा का उपयोग करने पर एक ट्यूटोरियल रियल वर्ल्ड हास्केल का हिस्सा है

जीसी सांख्यिकी

सबसे पहले, सुनिश्चित करें कि आप gh -O2 के साथ संकलन कर रहे हैं। और आप यह सुनिश्चित कर सकते हैं कि यह एक आधुनिक जीएचसी है (उदाहरण के लिए जीएचसी 6.12.x)

पहली चीज जो हम कर सकते हैं वह है कि कचरा संग्रहण समस्या नहीं है। + RTS -s के साथ अपना प्रोग्राम चलाएं

$ time ./A +RTS -s
./A +RTS -s 
749700
   9,961,432,992 bytes allocated in the heap
       2,463,072 bytes copied during GC
          29,200 bytes maximum residency (1 sample(s))
         187,336 bytes maximum slop
               **2 MB** total memory in use (0 MB lost due to fragmentation)

  Generation 0: 19002 collections,     0 parallel,  0.11s,  0.15s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time   13.15s  ( 13.32s elapsed)
  GC    time    0.11s  (  0.15s elapsed)
  RP    time    0.00s  (  0.00s elapsed)
  PROF  time    0.00s  (  0.00s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time   13.26s  ( 13.47s elapsed)

  %GC time       **0.8%**  (1.1% elapsed)

  Alloc rate    757,764,753 bytes per MUT second

  Productivity  99.2% of total user, 97.6% of total elapsed

./A +RTS -s  13.26s user 0.05s system 98% cpu 13.479 total

जो पहले से ही हमें बहुत सारी जानकारी देता है: आपके पास केवल 2M हीप है, और GC में 0.8% समय लगता है। इसलिए चिंता करने की जरूरत नहीं है कि आवंटन समस्या है।

समय प्रोफ़ाइल

अपने कार्यक्रम के लिए एक समय प्रोफ़ाइल प्राप्त करना सीधे आगे है: -ऑफ़ट -ऑटो-संकलन के साथ

 $ ghc -O2 --make A.hs -prof -auto-all
 [1 of 1] Compiling Main             ( A.hs, A.o )
 Linking A ...

और, एन = 200 के लिए:

$ time ./A +RTS -p                   
749700
./A +RTS -p  13.23s user 0.06s system 98% cpu 13.547 total

जो एक फ़ाइल बनाता है, A.prof, युक्त:

    Sun Jul 18 10:08 2010 Time and Allocation Profiling Report  (Final)

       A +RTS -p -RTS

    total time  =     13.18 secs   (659 ticks @ 20 ms)
    total alloc = 4,904,116,696 bytes  (excludes profiling overheads)

COST CENTRE          MODULE         %time %alloc

numDivs            Main         100.0  100.0

यह इंगित करते हुए कि आपका सारा समय सुन्नियों में बीता है, और यह आपके सभी आवंटन का स्रोत भी है।

ढेर प्रोफाइल

आप उन आवंटन का ब्रेक डाउन भी प्राप्त कर सकते हैं, + RTS -p -hy के साथ चलकर, जो कि Ahp बनाता है, जिसे आप इसे पोस्टस्क्रिप्ट फ़ाइल में परिवर्तित करके देख सकते हैं (hp2ps -c A.hp), जनरेट कर रहा है:

वैकल्पिक शब्द

जो हमें बताता है कि आपकी मेमोरी के उपयोग में कुछ भी गलत नहीं है: यह निरंतर स्थान में आवंटित कर रहा है।

तो आपकी समस्या numDivs की एल्गोरिथम जटिलता है:

toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2

उसे ठीक करें, जो आपके चलने के समय का 100% है, और बाकी सब कुछ आसान है।

अनुकूलन

यह अभिव्यक्ति स्ट्रीम फ़्यूज़न ऑप्टिमाइज़ेशन के लिए एक अच्छा उम्मीदवार है , इसलिए मैं इसे Data.Vector का उपयोग करने के लिए फिर से लिखूँगा , जैसे:

numDivs n = fromIntegral $
    2 + (U.length $
        U.filter (\x -> fromIntegral n `rem` x == 0) $
        (U.enumFromN 2 ((fromIntegral n `div` 2) + 1) :: U.Vector Int))

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

यह परीक्षण, ghc -O2 - Zake

$ time ./Z     
749700
./Z  3.73s user 0.01s system 99% cpu 3.753 total

तो यह एल्गोरिथ्म को बदलने के बिना, एन = 150 के लिए 3.5x तक चलने का समय कम कर दिया।

निष्कर्ष

आपकी समस्या स्तब्ध है। यह आपके चलने के समय का 100% है, और इसमें भयानक जटिलता है। संख्याओं के बारे में सोचें, और कैसे, उदाहरण के लिए, प्रत्येक N के लिए आप [2 .. n div2 + 1] N बार उत्पन्न कर रहे हैं । मानों को न बदलने के कारण, उसे याद रखने का प्रयास करें।

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


परिशिष्ट

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

हम ट्रायलिस्ट को भी फिर से लिख सकते हैं, और ट्रायल पर भरोसा करके इसे लूप में बदल सकते हैं, जिसे आप ट्रायल लिस्ट 2 में हाथ से लिखते हैं, जो कि "प्रीफिक्स स्कैन" फंक्शन (उर्फ स्कैनल) है:

triaList = U.scanl (+) 0 (U.enumFrom 1 top)
    where
       top = 10^6

इसी तरह सोल के लिए:

sol :: Int -> Int
sol n = U.head $ U.filter (\x -> numDivs x > n) triaList

एक ही समग्र समय चल रहा है, लेकिन एक बिट क्लीनर कोड के साथ।


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

1
भविष्य के उपयोगकर्ताओं के लिए, के -auto-allपक्ष में पदावनत किया जाता है -fprof-auto
बी। मेहता

60

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

आपके द्वारा दिए गए कोड (*) के साथ एक उदाहरण है, हरा ठीक है, लाल धीमा है: वैकल्पिक शब्द

सभी समय विभाजकों की सूची बनाने में जाता है। इससे आपको कुछ चीजें पता चल सकती हैं:
1. फ़िल्टरिंग n rem x == 0तेज़ करें, लेकिन चूंकि यह एक अंतर्निहित कार्य है, इसलिए यह पहले से ही तेज़ है।
2. एक छोटी सूची बनाएँ। आपने पहले से ही जाँच करके उस दिशा में कुछ किया है n quot 2
3. सूची पीढ़ी को पूरी तरह से फेंक दें और तेजी से समाधान प्राप्त करने के लिए कुछ गणित का उपयोग करें। प्रोजेक्ट यूलर समस्याओं के लिए यह सामान्य तरीका है।

(*) मैंने eu13.hsएक मुख्य फ़ंक्शन को जोड़ते हुए, फ़ाइल में अपना कोड डालकर इसे प्राप्त किया main = print $ sol 90। फिर चल रहा है visual-prof -px eu13.hs eu13और परिणाम में है eu13.hs.html


3

हास्केल संबंधित नोट: triaList2निश्चित रूप से तेजी से होता है triaListक्योंकि बाद वाला बहुत अधिक अनावश्यक संगणना करता है। यह n के पहले तत्वों की गणना करने के लिए द्विघात समय लेगा triaList, लेकिन इसके लिए रैखिक triaList2। त्रिकोण संख्याओं की अनंत आलसी सूची को परिभाषित करने का एक और सुरुचिपूर्ण (और कुशल) तरीका है:

triaList = 1 : zipWith (+) triaList [2..]

गणित से संबंधित नोट: n / 2 तक सभी विभाजकों की जांच करने की आवश्यकता नहीं है, यह sqrt (n) तक की जांच करने के लिए पर्याप्त है।


2
इस पर भी विचार करें: स्कैनल (+) १ [२ ..]
डॉन स्टीवर्ट

1

आप समय रूपरेखा को सक्षम करने के लिए झंडे के साथ अपना कार्यक्रम चला सकते हैं। कुछ इस तरह:

./program +RTS -P -sprogram.stats -RTS

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


1
लेकिन पहले इसे संकलित करेंghc -prof -auto-all -fforce-recomp --make -O2 program.hs
डैनियल
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.