2011-12-28 23 views
18

स्पोइलर चेतावनी: यह प्रोजेक्ट यूलर से Problem 14 से संबंधित है।यह सरल हैकेल एल्गोरिदम इतना धीमा क्यों है?

निम्नलिखित कोड को चलाने के लिए लगभग 15s लगते हैं। मेरे पास एक गैर-रिकर्सिव जावा समाधान है जो 1 एस में चलता है। मुझे लगता है कि मुझे इस कोड को बहुत करीब मिलना चाहिए।

import Data.List 

collatz a 1 = a 
collatz a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

main = do 
    print ((foldl1' max) . map (collatz 1) $ [1..1000000]) 

मैं +RHS -p साथ प्रोफाइल और पाया है कि आबंटित स्मृति बड़ी है, और के रूप में इनपुट बढ़ता बढ़ता है। n = 100,000 1 जीबी आवंटित (!) के लिए n = 1,000,000 13 जीबी (!!) आवंटित किया गया है।

फिर फिर, -sstderr दिखाता है कि हालांकि कई बाइट आवंटित किए गए थे, कुल स्मृति उपयोग 1 एमबी था, और उत्पादकता 95% + थी, इसलिए शायद 13 जीबी लाल हेरिंग है।

मैं कुछ संभावनाओं के बारे में सोच सकते हैं:

  1. कुछ के रूप में सख्त रूप में यह करने की जरूरत नहीं है। मैंने पहले से ही foldl1' खोजा है, लेकिन शायद मुझे और करने की आवश्यकता है? यह संभव चिह्नित करने के लिए collatz के रूप में सख्त (यह है कि कोई मतलब है?

  2. collatz है पूंछ-कॉल के अनुकूलन नहीं। मैं यह होना चाहिए लगता है लेकिन पुष्टि करने के लिए एक तरह से पता नहीं है है।

  3. संकलक कुछ अनुकूलन नहीं कर रहा है मुझे लगता है कि यह होना चाहिए - उदाहरण के के लिए केवल collatz के दो परिणाम किसी भी एक समय (अधिकतम और वर्तमान)

कोई सुझाव पर स्मृति में होने की जरूरत है

?

यह Why is this Haskell expression so slow? का एक डुप्लिकेट है, हालांकि मुझे लगता है कि तेज़ जावा समाधान को कोई भी ज्ञापन करने की आवश्यकता नहीं है। क्या इसका उपयोग करने के बिना इसे गति देने के कोई तरीके हैं?

Wed Dec 28 09:33 2011 Time and Allocation Profiling Report (Final) 

    scratch +RTS -p -hc -RTS 

    total time =  5.12 secs (256 ticks @ 20 ms) 
    total alloc = 13,229,705,716 bytes (excludes profiling overheads) 

COST CENTRE     MODULE    %time %alloc 

collatz      Main     99.6 99.4 


                           individual inherited 
COST CENTRE    MODULE            no. entries %time %alloc %time %alloc 

MAIN      MAIN             1   0 0.0 0.0 100.0 100.0 
CAF      Main             208   10 0.0 0.0 100.0 100.0 
    collatz    Main             215   1 0.0 0.0  0.0 0.0 
    main     Main             214   1 0.4 0.6 100.0 100.0 
    collatz    Main             216   0 99.6 99.4 99.6 99.4 
CAF      GHC.IO.Handle.FD          145   2 0.0 0.0  0.0 0.0 
CAF      System.Posix.Internals        144   1 0.0 0.0  0.0 0.0 
CAF      GHC.Conc            128   1 0.0 0.0  0.0 0.0 
CAF      GHC.IO.Handle.Internals        119   1 0.0 0.0  0.0 0.0 
CAF      GHC.IO.Encoding.Iconv        113   5 0.0 0.0  0.0 0.0 

और -sstderr:

./scratch +RTS -sstderr 
525 
    21,085,474,908 bytes allocated in the heap 
     87,799,504 bytes copied during GC 
      9,420 bytes maximum residency (1 sample(s))   
      12,824 bytes maximum slop    
       1 MB total memory in use (0 MB lost due to fragmentation) 

    Generation 0: 40219 collections,  0 parallel, 0.40s, 0.51s elapsed 
    Generation 1:  1 collections,  0 parallel, 0.00s, 0.00s elapsed 

    INIT time 0.00s ( 0.00s elapsed) 
    MUT time 35.38s (36.37s elapsed) 
    GC time 0.40s ( 0.51s elapsed) 
    RP time 0.00s ( 0.00s elapsed) PROF time 0.00s ( 0.00s elapsed) 
    EXIT time 0.00s ( 0.00s elapsed) 
    Total time 35.79s (36.88s elapsed) %GC time  1.1% (1.4% elapsed) Alloc rate 595,897,095 bytes per MUT second 

    Productivity 98.9% of total user, 95.9% of total elapsed 

और जावा समाधान (मेरा नहीं, परियोजना यूलर मंचों से लिया Memoization के साथ हटा दिया):

संदर्भ के लिए, मेरी रूपरेखा उत्पादन होता है

public class Collatz { 
    public int getChainLength(int n) 
    { 
    long num = n; 
    int count = 1; 
    while(num > 1) 
    { 
     num = (num%2 == 0) ? num >> 1 : 3*num+1; 
     count++; 
    } 
    return count; 
    } 

    public static void main(String[] args) { 
    Collatz obj = new Collatz(); 
    long tic = System.currentTimeMillis(); 
    int max = 0, len = 0, index = 0; 
    for(int i = 3; i < 1000000; i++) 
    { 
     len = obj.getChainLength(i); 
     if(len > max) 
     { 
     max = len; 
     index = i; 
     } 
    } 
    long toc = System.currentTimeMillis(); 
    System.out.println(toc-tic); 
    System.out.println("Index: " + index + ", length = " + max); 
    } 
} 
+0

यह आश्चर्यजनक है कि जीएचसी किसी भी आत्म-सम्मानित सी कंपाइलर की अपेक्षा करने की अपेक्षा नहीं करेगा (उद्धरण एन 2) (rshift n 1)। क्या कोई कारण है? –

+0

@ सोर्सिज: यह मुझे भी हैरान कर दिया। – ehird

उत्तर

20

पहले, मैंने सोचा था कि आपको से पहले विस्मयादिबोधक चिह्न डालने का प्रयास करना चाहिए एकcollatz में:

collatz !a 1 = a 
collatz !a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

इस प्रकार है (आप अपने स्रोत फ़ाइल के शीर्ष पर {-# LANGUAGE BangPatterns #-} डाल करने के लिए यह काम करने के लिए की आवश्यकता होगी।)

मेरे तर्क चला गया: समस्या यह है कि आप कर रहे हैं है कोलात्ज़ के पहले तर्क में थंक का निर्माण: यह 1 के रूप में शुरू होता है, और फिर 1 + 1 बन जाता है, और फिर (1 + 1) + 1 बन जाता है, ... बिना किसी मजबूर किए।यह bang pattern किसी भी कॉल किए जाने पर मजबूर होने के लिए collatz के पहले तर्क को बल देता है, इसलिए यह 1 के रूप में शुरू होता है, और फिर 2 बन जाता है, और इसी तरह, बिना किसी अपरिचित थंक के निर्माण के: यह केवल एक पूर्णांक के रूप में रहता है।

ध्यान दें कि seq का उपयोग करने के लिए एक बैंग पैटर्न सिर्फ शॉर्टेंड है; इस मामले में, हम collatz इस प्रकार पुनर्लेखन सकता है:

collatz a _ | seq a False = undefined 
collatz a 1 = a 
collatz a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

चाल यहाँ गार्ड, जो तब हमेशा गलत का आकलन (और इसलिए शरीर अप्रासंगिक है) में एक मजबूर करने के लिए है। फिर अगले मामले के साथ मूल्यांकन जारी रहता है, का मूल्यांकन पहले से ही किया जा चुका है। हालांकि, एक धमाका पैटर्न स्पष्ट है।

दुर्भाग्यवश, -O2 के साथ संकलित होने पर, यह मूल से कहीं अधिक तेज़ नहीं चलता है! हम और क्या कोशिश कर सकते हैं? खैर, एक बात हम कर सकते हैं यह मान रहा है कि दो नंबर एक मशीन के आकार पूर्णांक अतिप्रवाह कभी नहीं, और collatz इस प्रकार एनोटेशन दे:

collatz :: Int -> Int -> Int 

हम, वहाँ धमाके पैटर्न छोड़ के बाद से हम अभी भी इमारत से बचना चाहिए करेंगे ऊपर thunks, भले ही वे प्रदर्शन की समस्या की जड़ नहीं हैं। यह मेरे (धीमे) कंप्यूटर पर समय को 8.5 सेकंड तक लाता है।

अगला चरण जावा समाधान के करीब लाने का प्रयास करना है। एहसास करने वाली पहली बात यह है कि हास्केल में, div नकारात्मक पूर्णांक के संबंध में अधिक गणितीय रूप से सही तरीके से व्यवहार करता है, लेकिन "सामान्य" सी विभाजन से धीमा है, जिसे हास्केल में quot कहा जाता है। को quot के साथ प्रतिस्थापित करने के लिए रनटाइम को 5.2 सेकंड तक लाया गया, और x `quot` 2 को x `shiftR` 1 (डेटा.बिट्स आयात करने) के साथ जावा समाधान से मेल खाने के लिए इसे 4.9 सेकेंड तक लाया गया।

यह उतना ही कम है जितना मैं इसे अभी प्राप्त कर सकता हूं, लेकिन मुझे लगता है कि यह एक बहुत अच्छा परिणाम है; चूंकि आपका कंप्यूटर मेरी तुलना में तेज़ है, इसलिए यह जावा समाधान के करीब भी होना चाहिए।

यहाँ अंतिम कोड (मैं रास्ते पर सफाई का एक छोटा सा किया था) है:

{-# LANGUAGE BangPatterns #-} 

import Data.Bits 
import Data.List 

collatz :: Int -> Int 
collatz = collatz' 1 
    where collatz' :: Int -> Int -> Int 
     collatz' !a 1 = a 
     collatz' !a x 
      | even x = collatz' (a + 1) (x `shiftR` 1) 
      | otherwise = collatz' (a + 1) (3 * x + 1) 

main :: IO() 
main = print . foldl1' max . map collatz $ [1..1000000] 

इस कार्यक्रम के लिए GHC कोर (ghc-core के साथ) को देखते हुए, मुझे लगता है कि यह शायद के रूप में के बारे में है अच्छा हो जाता है; collatz लूप अनबॉक्स किए गए पूर्णांक का उपयोग करता है और शेष प्रोग्राम ठीक दिखता है। एकमात्र सुधार जो मैं सोच सकता हूं, map collatz [1..1000000] पुनरावृत्ति से मुक्केबाजी को समाप्त कर देगा।

वैसे, "कुल आवंटन" आंकड़े के बारे में चिंता न करें; कार्यक्रम के जीवनकाल में आवंटित कुल स्मृति है, और जब भी जीसी उस स्मृति को पुनः प्राप्त करता है तब भी यह कभी घटता नहीं है। एकाधिक टेराबाइट्स के आंकड़े आम हैं।

+0

धन्यवाद, यह वास्तव में सहायक है। मुझे '-O2' के बारे में पता नहीं था, जो एक बड़ा अंतर बनाता है (रनटाइम 5s तक गिर जाता है)। प्रश्न के लिए जावा समाधान जोड़ा गया। –

+0

ओह, मुझे लगता है कि आप पहले ही '-O2' का उपयोग कर रहे थे, क्योंकि मेरी मशीन पर बैंग पैटर्न के साथ संशोधित प्रोग्राम 16 सेकंड में चला गया :) मैं आपके जावा समाधान पर एक नज़र डालूंगा। – ehird

+0

ठीक है, मैंने आगे के सुधार के साथ इस उत्तर को अद्यतन किया है। – ehird

2

आप सूची और बैंग पैटर्न खो सकते हैं और फिर भी स्टैक का उपयोग करके वही प्रदर्शन प्राप्त कर सकते हैं। इस के साथ

import Data.List 
import Data.Bits 

coll :: Int -> Int 
coll 0 = 0 
coll 1 = 1 
coll 2 = 2 
coll n = 
    let a = coll (n - 1) 
     collatz a 1 = a 
     collatz a x 
     | even x = collatz (a + 1) (x `shiftR` 1) 
     | otherwise = collatz (a + 1) (3 * x + 1) 
    in max a (collatz 1 n) 


main = do 
    print $ coll 100000 

एक समस्या यह है कि आप 1_000_000 की तरह, बड़े आदानों के लिए ढेर के आकार बढ़ाने के लिए होगा।

अद्यतन:

यहाँ एक पूंछ पुनरावर्ती संस्करण है कि ढेर अतिप्रवाह समस्या से ग्रस्त नहीं है।

import Data.Word 
collatz :: Word -> Word -> (Word, Word) 
collatz a x 
    | x == 1 = (a,x) 
    | even x = collatz (a + 1) (x `quot` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

coll :: Word -> Word 
coll n = collTail 0 n 
    where 
    collTail m 1 = m 
    collTail m n = collTail (max (fst $ collatz 1 n) m) (n-1) 

सूचना Word बजाय Int का उपयोग। यह प्रदर्शन में एक फर्क पड़ता है। यदि आप चाहें तो भी आप बैंग पैटर्न का उपयोग कर सकते हैं, और यह प्रदर्शन को लगभग दोगुना कर देगा।

0

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

collatz n = if even n then n `div` 2 else 3 * n + 1 

पुनर्लेखन

collatz n = case n `divMod` 2 of 
      (n', 0) -> n' 
      _  -> 3 * n + 1 

के रूप में 1.2 सेकंड एक 2.8 गीगा Athlon द्वितीय X4 430 सीपीयू के साथ एक सिस्टम पर मेरा कार्यक्रम के लिए क्रम उड़ान भरी। मेरे प्रारंभिक तेजी संस्करण (2.3 सेकंड divMod के उपयोग के बाद):

{-# LANGUAGE BangPatterns #-} 

import Data.List 
import Data.Ord 

collatzChainLen :: Int -> Int 
collatzChainLen n = collatzChainLen' n 1 
    where collatzChainLen' n !l 
      | n == 1 = l 
      | otherwise = collatzChainLen' (collatz n) (l + 1) 

collatz:: Int -> Int 
collatz n = case n `divMod` 2 of 
       (n', 0) -> n' 
       _  -> 3 * n + 1 

pairMap :: (a -> b) -> [a] -> [(a, b)] 
pairMap f xs = [(x, f x) | x <- xs] 

main :: IO() 
main = print $ fst (maximumBy (comparing snd) (pairMap collatzChainLen [1..999999])) 

एक 9.7 सेकंड (divMod साथ 8.5) में शायद अधिक मुहावरेदार हास्केल संस्करण रन; इसके लिए

collatzChainLen :: Int -> Int 
collatzChainLen n = 1 + (length . takeWhile (/= 1) . (iterate collatz)) n 

Data.List.Stream का उपयोग बचाने के समान है धारा संलयन कि इस संस्करण रन है कि स्पष्ट संचय के साथ की तरह अधिक होगा अनुमति देने के लिए माना जाता है, लेकिन मैं एक Ubuntu libghc * पैकेज है कि नहीं मिल सकता है Data.List.Stream, इसलिए मैं अभी तक इसे सत्यापित नहीं कर सकता।

संबंधित मुद्दे