Narzędzia do analizy wydajności programu Haskell

Podczas rozwiązywania niektórych problemów projektu Euler nauczyć Haskell (więc obecnie jestem całkowicie początkujący) i przyszedł Problem 13 . Napisałem to (naiwne) rozwiązanie:

--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

To rozwiązanie dla n=500 (sol 500) jest ekstremalnie wolne (działa już ponad 2 godziny), więc zastanawiałem się, jak dowiedzieć się, dlaczego to rozwiązanie jest tak wolne. Czy są jakieś polecenia, które mówią mi, gdzie spędza się większość czasu obliczeniowego, abym wiedział, która część mojego programu haskell jest wolna? Coś w stylu prostego profiler.

Aby było jasne, nie proszę oszybsze rozwiązanie, ale o sposób, aby znaleźć To rozwiązanie. Jak zacząłbyś, gdybyś nie miał wiedzy Haskella?

Próbowałem napisać dwie funkcje trialistyczne, ale nie znalazłem sposobu, aby sprawdzić, która z nich jest szybsza, więc od tego zaczynają się moje problemy.

Thanks

Author: Don Stewart, 2010-07-18

4 answers

Jak dowiedzieć się, dlaczego to rozwiązanie jest tak powolne. Czy są jakieś polecenia, które mówią mi, gdzie spędza się większość czasu obliczeniowego, abym wiedział, która część mojego programu haskell jest wolna?

Dokładnie! GHC zapewnia wiele doskonałych narzędzi, w tym:

Samouczek na temat korzystania z profilowania w czasie i przestrzeni jest częścią realnego świata Haskell .

Statystyki GC

Po pierwsze, upewnij się, że kompilujesz z ghc-O2. I możesz się upewnić, że jest to nowoczesny GHC (np. GHC 6.12.x)

Pierwszą rzeczą, którą możemy zrobić, to sprawdzić, czy wywóz śmieci nie jest problemem. Uruchom program z + 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

Co daje nam już wiele informacji: masz tylko 2M sterty, a GC zajmuje 0,8% czasu. Więc nie musisz się martwić, że alokacja jest problemem.

Profile Czasowe

Uzyskanie profilu czasu dla Twojego programu jest proste: skompiluj za pomocą-prof-auto-all

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

I, dla N=200:

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

Który tworzy plik, A. prof, zawierający:

    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

Wskazując, że cały twój czas spędzony jest w numDivs, jest to również źródło wszystkich Twoich przydziałów.

Profile Sterty

Możesz również uzyskać podział tych alokacji, uruchamiając z +RTS-p-hy, który tworzy A. hp, który możesz wyświetlić, konwertując go do pliku postscriptowego (hp2ps-c A. hp), generując:

alt text

Co mówi nam, że nie ma nic złego w używaniu pamięci: jest alokowana w stałej przestrzeni.

Więc Twoim problemem jest algorytmiczna złożoność numDivs:

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

Napraw to, co jest 100% twojego czasu pracy, a Wszystko inne jest łatwe.

Optymalizacje

To wyrażenie jest dobrym kandydatem do optymalizacjistream fusion , więc napiszę je ponownie aby użyć danych.Wektor , tak:

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))

Który powinien połączyć się w jedną pętlę bez zbędnych przydziałów sterty. Oznacza to, że będzie miała lepszą złożoność (przez stałe czynniki) niż wersja listy. Możesz użyć ghc-core tool (dla zaawansowanych użytkowników) do kontroli kodu pośredniego po optymalizacji.

Testowanie tego, ghc-O2 -- make Z. hs

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

Skróciła więc czas pracy dla N=150 o 3,5 x, bez zmiany samego algorytmu.

Wniosek

Twój problem to numDivs. Jest to 100% twojego czasu pracy i ma straszną złożoność. pomyśl o numdivach i jak, na przykład, dla każdego N generujesz [2 .. n div 2 + 1] N razy. Spróbuj zapamiętać to, skoro wartości się nie zmieniają.

Aby zmierzyć, która z twoich funkcji jest szybsza, rozważ użycie kryterium , które dostarczy statystycznie solidnych informacji o ulepszeniach sub-mikrosekund w czasie działania.


Addenda

Ponieważ numDivs to 100% twojego czasu pracy, dotykanie innych części programu nie będzie miało większego znaczenia, jednak dla celów pedagogicznych możemy również przepisać te, które używają strumienia fuzja.

Możemy również przepisać triallistę i polegać na fusion, aby przekształcić ją w pętlę, którą piszesz ręcznie w trialList2, co jest funkcją "skanowania przedrostkowego" (aka scanl):

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

Podobnie dla sol:

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

Z tym samym ogólnym czasem pracy, ale nieco czystszym kodem.

 178
Author: Don Stewart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:33:26

Odpowiedź Donsa jest świetna, nie będąc spojlerem, dając bezpośrednie rozwiązanie problemu.
Tutaj chcę zasugerować małe narzędzie , które napisałem niedawno. Oszczędza to czas na ręczne pisanie adnotacji SCC, gdy chcesz mieć bardziej szczegółowy profil niż domyślny ghc -prof -auto-all. Poza tym jest kolorowy!

Oto przykład z kodem, który podałeś (*), zielony jest OK, czerwony jest wolny: alt text

Cały czas tworzy listę dzielników. To sugeruje kilka rzeczy można zrobić:
1. Spraw, aby filtrowanie n rem x == 0 było szybsze, ale ponieważ jest to wbudowana funkcja, prawdopodobnie jest już szybka.
2. Utwórz krótszą listę. Zrobiłeś już coś w tym kierunku, sprawdzając tylko do n quot 2.
3. Wyrzuć generowanie listy całkowicie i użyj trochę matematyki, aby uzyskać szybsze rozwiązanie. Jest to typowy sposób rozwiązywania problemów Eulera.

(*) uzyskałem to, umieszczając Twój kod w pliku o nazwie eu13.hs, dodając główną funkcję main = print $ sol 90. Następnie uruchamiamy visual-prof -px eu13.hs eu13 i wynik jest w eu13.hs.html.

 57
Author: Daniel Velkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-23 19:33:15

Uwaga związana z Haskellem: triaList2 jest oczywiście szybsza niż triaList, ponieważ ta ostatnia wykonuje wiele niepotrzebnych obliczeń. Obliczenie n pierwszych elementów triaList zajmie kwadratyczny czas, ale liniowy dla triaList2. Istnieje jeszcze jeden elegancki (i skuteczny) sposób na zdefiniowanie nieskończonej leniwej listy liczb trójkąta:

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

Uwaga związana z matematyką: nie ma potrzeby sprawdzania wszystkich dzielników do n / 2, wystarczy sprawdzić do sqrt (n).

 3
Author: rkhayrov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-18 16:54:24

Możesz uruchomić swój program z flagami, aby włączyć profilowanie czasu. Coś takiego:

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

To powinno uruchomić program i stworzyć plik o nazwie program.statystyki, które będą miały ile czasu zostało spędzone w każdej funkcji. Więcej informacji na temat profilowania za pomocą GHC można znaleźć w GHC user guide. Dla benchmarkingu istnieje Biblioteka kryterium. Znalazłem Ten blog ma przydatne wprowadzenie.

 1
Author: user394827,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-18 17:56:04