Haskell programının performansını analiz etmek için araçlar


104

Haskell'i öğrenmek için bazı Project Euler Problemlerini çözerken (yani şu anda tamamen yeni başlayan biriyim) Problem 12'ye geldim . Bu (saf) çözümü yazdım:

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

Bu Çözüm n=500 (sol 500)son derece yavaş (şu anda 2 saatten fazla çalışıyor), bu yüzden bu çözümün neden bu kadar yavaş olduğunu nasıl bulacağımı merak ettim. Bana hesaplama süresinin çoğunun nerede harcandığını söyleyen herhangi bir komut var mı, böylece haskell programımın hangi bölümünün yavaş olduğunu öğrenebilir miyim? Basit bir profil oluşturucu gibi bir şey.

Açıklığa kavuşturmak için , daha hızlı bir çözüm değil, bu çözümü bulmanın bir yolunu istiyorum . Haskell bilginiz olmasa nasıl başlarsınız?

İki triaListfonksiyon yazmaya çalıştım ama hangisinin daha hızlı olduğunu test etmenin bir yolunu bulamadım, bu yüzden sorunlarımın başladığı yer burası.

Teşekkürler

Yanıtlar:


187

bu çözümün neden bu kadar yavaş olduğunu nasıl öğrenebilirim? Bana hesaplama zamanının çoğunun nerede harcandığını söyleyen herhangi bir komut var mı, böylece haskell programımın hangi bölümünün yavaş olduğunu öğrenebilir miyim?

Tam! GHC, aşağıdakiler dahil birçok mükemmel araç sağlar:

Zaman ve mekan profili oluşturma hakkında bir eğitim, Real World Haskell'in bir parçasıdır .

GC İstatistikleri

İlk olarak, ghc -O2 ile derlediğinizden emin olun. Ve bunun modern bir GHC olduğundan emin olabilirsiniz (ör. GHC 6.12.x)

Yapabileceğimiz ilk şey, çöp toplamanın sorun olmadığını kontrol etmektir. Programınızı + RTS -s ile çalıştırın

$ 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

Bu da bize zaten çok fazla bilgi veriyor: yalnızca 2M'lik bir yığınınız var ve GC zamanın% 0,8'ini kaplıyor. Bu nedenle, sorunun tahsisat olduğu konusunda endişelenmenize gerek yok.

Zaman Profilleri

Programınız için bir zaman profili elde etmek basittir: -prof -auto-all ile derleyin

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

Ve N = 200 için:

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

bu, aşağıdakileri içeren bir A.prof dosyası oluşturur:

    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

Belirten bütün zaman numDivs harcandığını ve aynı zamanda tüm ayırmaları kaynağıdır.

Yığın Profilleri

Ayrıca, aşağıdakileri oluşturarak bir postscript dosyasına (hp2ps -c A.hp) dönüştürerek görüntüleyebileceğiniz A.hp'yi oluşturan + RTS -p -hy ile çalıştırarak bu tahsislerin dökümünü alabilirsiniz:

alternatif metin

bu bize bellek kullanımınızda yanlış bir şey olmadığını söyler: sabit alanda ayırma.

Yani probleminiz numDivs'in algoritmik karmaşıklığı:

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

Çalıştırma sürenizin% 100'ü olan bunu düzeltin ve diğer her şey kolaydır.

Optimizasyonlar

Bu ifade, akış füzyon optimizasyonu için iyi bir adaydır , bu yüzden Data.Vector'ı kullanmak için yeniden yazacağım , şöyle:

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

Gereksiz yığın ayırmaları olmadan tek bir döngüde birleşmelidir. Yani, liste sürümünden (sabit faktörlere göre) daha iyi karmaşıklığa sahip olacaktır. Optimizasyondan sonra ara kodu incelemek için ghc-core aracını (ileri düzey kullanıcılar için) kullanabilirsiniz.

Bunu test ediyorum, ghc -O2 - Z.hs yapın

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

Böylece, algoritmanın kendisini değiştirmeden, N = 150 için çalışma süresini 3,5x azalttı.

Sonuç

Senin sorunun numDivs. Çalıştırma sürenizin% 100'üdür ve korkunç bir karmaşıklığa sahiptir. NumDiv'leri düşünün ve örneğin her N için nasıl [2 .. n div2 + 1] N kez ürettiğinizi düşünün . Değerler değişmediği için bunu hatırlamayı deneyin.

Hangi işlevlerinizin daha hızlı olduğunu ölçmek için , çalışma süresindeki mikrosaniyenin altındaki iyileştirmeler hakkında istatistiksel olarak sağlam bilgiler sağlayacak olan ölçüt kullanmayı düşünün .


Addenda

NumDivs, çalışma sürenizin% 100'ü olduğundan, programın diğer bölümlerine dokunmak pek bir fark yaratmayacaktır, ancak pedagojik amaçlar için, bunları stream fusion kullanarak yeniden yazabiliriz.

Ayrıca, deneme Listesini yeniden yazabilir ve bir "önek tarama" işlevi olan (diğer adıyla scanl) deneme Listesi2'de elle yazdığınız döngüye dönüştürmek için füzyona güvenebiliriz:

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

Sol için benzer şekilde:

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

Aynı genel çalışma süresine sahip, ancak biraz daha temiz bir kod.


Benim gibi diğer salaklar için bir not: timeDon'un Zaman Profillerinde bahsettiği yardımcı timeprogram sadece Linux programıdır. Windows'ta mevcut değildir. Dolayısıyla, Windows'ta (aslında herhangi bir yerde) zaman profili oluşturmak için bu soruya bakın .
John Red

1
Gelecekteki kullanıcılar için, -auto-alllehine kullanımdan kaldırılmıştır -fprof-auto.
B. Mehta

60

Dons'un cevabı, soruna doğrudan bir çözüm sunarak spoiler olmadan harika.
Burada son zamanlarda yazdığım küçük bir araç önermek istiyorum . Varsayılandan daha ayrıntılı bir profil istediğinizde, SCC açıklamalarını elle yazmak için size zaman kazandırır ghc -prof -auto-all. Bunun yanı sıra renkli!

İşte verdiğiniz kodla (*) bir örnek, yeşil tamam, kırmızı yavaş: alternatif metin

Her zaman bölenler listesi oluşturmaya gider. Bu, yapabileceğiniz birkaç şeyi önerir:
1. Filtrelemeyi n rem x == 0daha hızlı yapın, ancak yerleşik bir işlev olduğu için muhtemelen zaten hızlıdır.
2. Daha kısa bir liste oluşturun. Sadece kadar kontrol ederek bu yönde bir şeyler yaptınız n quot 2.
3. Liste oluşturmayı tamamen ortadan kaldırın ve daha hızlı bir çözüm elde etmek için biraz matematik kullanın. Bu, proje Euler problemlerinin olağan yoludur.

(*) Bunu, kodunuzu adlı bir dosyaya koyarak eu13.hsana işlev ekleyerek aldım main = print $ sol 90. Sonra koşuyor visual-prof -px eu13.hs eu13ve sonuç geliyor eu13.hs.html.


3

Haskell ile ilgili not: triaList2tabii ki daha hızlıdır triaListçünkü ikincisi çok fazla gereksiz hesaplama yapar. N ilk elemanını hesaplamak ikinci dereceden zaman alacaktır triaList, ancak için doğrusal triaList2. Üçgen sayılarının sonsuz ve tembel bir listesini tanımlamanın başka bir zarif (ve etkili) yolu daha var:

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

Matematikle ilgili not: n / 2'ye kadar tüm bölenleri kontrol etmeye gerek yoktur, sqrt (n) 'ye kadar kontrol etmek yeterlidir.


2
Ayrıca şunu da göz önünde bulundurun: scanl (+) 1 [2 ..]
Don Stewart

1

Zaman profili oluşturmayı etkinleştirmek için programınızı bayraklarla çalıştırabilirsiniz. Bunun gibi bir şey:

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

Bu, programı çalıştırmalı ve her işlevde ne kadar zaman harcandığını gösteren program.stats adlı bir dosya üretmelidir. GHC kullanım kılavuzunda GHC ile profil oluşturma hakkında daha fazla bilgi bulabilirsiniz . Kıyaslama için Criterion kütüphanesi var. Bu blog gönderisinin yararlı bir girişi olduğunu buldum .


1
Ama önce onu derleyinghc -prof -auto-all -fforce-recomp --make -O2 program.hs
Daniel
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.