Outils d'analyse de la performance D'un programme Haskell

tout en résolvant certains problèmes D'Euler projet d'apprendre Haskell (donc actuellement je suis un débutant complet) je suis venu plus problème 13 . J'ai écrit Cette solution (naïve):

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

Cette Solution pour n=500 sol (500) est extrêmement lent (plus de 2 heures maintenant), donc je me demandais comment savoir pourquoi cette solution est si lent. Existe-il des commandes dites-moi où la plupart des calculs de temps donc je sais quelle partie de mon Haskell-le programme est lent? Quelque chose comme un simple profileur.

pour être clair, Je ne demande pas pour une solution plus rapide mais pour un moyen pour trouver cette solution. Comment commenceriez-vous si vous n'aviez aucune connaissance de haskell?

j'ai essayé d'écrire deux fonctions trialistes, mais je n'ai trouvé aucun moyen de tester laquelle est la plus rapide, donc c'est là que mes problèmes commencent.

Merci

98
demandé sur Don Stewart 2010-07-18 20:09:31

4 réponses

comment savoir pourquoi cette solution est si lent. Y a-t-il des commandes qui me disent où passe la plupart du temps de calcul pour que je sache quelle partie de mon programme haskell est lente?

précisément! GHC fournit de nombreux excellents outils, y compris:

un tutoriel sur l'utilisation du profilage temporel et spatial est partie du monde réel Haskell .

statistiques GC

tout d'abord, assurez-vous de compiler avec ghc-O2. Et vous pourriez vous assurer qu'il s'agit d'un GHC moderne (par exemple GHC 6.12.x)

La première chose que nous pouvons faire est de vérifier que la collecte des ordures n'est pas le problème. Exécutez votre programme avec +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

qui nous donne déjà beaucoup d'informations: vous n'avez qu'un tas de 2M, et GC prend 0,8% du temps. Donc pas de il faut s'inquiéter que la répartition soit le problème.

Temps Des Profils

obtenir un profil de temps pour votre programme est simple: compiler avec-prof-auto-all

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

et, pour N = 200:

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

qui crée un fichier, A. prof, contenant:

    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

indiquant que tout votre temps est passé dans numDivs, et c'est aussi la source de toutes vos allocations.

Tas De Profils

vous pouvez également obtenir une ventilation de ces allocations, en exécutant avec +RTS-p-hy, qui crée A. hp, que vous pouvez visualiser en le convertissant en un fichier postscript (hp2ps-C. A. hp), générant:

alt text

qui nous dit qu'il n'y a rien de mal à utiliser votre mémoire: répartition en espace constant.

donc votre problème est la complexité algorithmique de numDivs:

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

réparez ça, ce qui représente 100% de votre temps de course, et tout le reste est facile.

optimisations

cette expression est un bon candidat pour l'optimisation stream fusion , donc je vais la réécrire pour utiliser les données .Vecteur , comme ceci:

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

qui devrait fusionner en une seule boucle sans allocation de tas inutile. C'est, il aura une meilleure complexité (par des facteurs constants) que la version de liste. Vous pouvez utiliser l'outil ghc-core (pour les utilisateurs avancés) pour inspecter le code intermédiaire après optimisation.

Testing this, ghc-O2 -- make Z. hs

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

de sorte qu'il a réduit la durée de fonctionnement pour N=150 de 3,5 x, sans changer le algorithme lui-même.

Conclusion

votre problème est numDivs. C'est 100% de votre temps de course, et a terrible complexité. pensez à numDivs, et comment, par exemple, pour chaque N que vous générez [2 .. n div 2 + 1]N fois. Essayez de mémoriser ça, puisque les valeurs ne changent pas.

pour mesurer laquelle de vos fonctions est la plus rapide, envisagez d'utiliser critère , qui fournira des informations statistiquement robustes sur les améliorations sous-microsecondes du temps de fonctionnement.


Addenda

puisque numDivs est 100% de votre temps de fonctionnement, toucher d'autres parties du programme ne fera pas beaucoup de différence, cependant, à des fins pédagogiques, nous pouvons également réécrire ceux qui utilisent stream fusion.

on peut aussi réécrire trialList, et de s'appuyer sur la fusion pour en faire la boucle de vous écrire à la main dans trialList2, qui est une fonction "prefix scan" (alias scanl):

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

de même pour sol:

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

avec le même temps de fonctionnement global, mais un peu plus de code.

178
répondu Don Stewart 2017-05-23 11:33:26

la réponse de Dons est grande sans être un spoiler en donnant une solution directe au problème.

Ici, je veux suggérer un petit outil que j'ai écrit récemment. Cela vous épargne le temps d'écrire des annotations SCC à la main lorsque vous voulez un profil plus détaillé que le profil par défaut ghc -prof -auto-all . Outre qu'il est coloré!

voici un exemple avec le code que vous avez donné (*), le vert est OK, le rouge est lent: alt text

tout le temps va dans la création de la liste des diviseurs. Ceci suggère quelques choses que vous pouvez faire:

1. Faites le filtrage n rem x == 0 plus rapide, mais puisque c'est une fonction intégrée probablement il est déjà rapide.

2. Créez une liste plus courte. Vous avez déjà fait quelque chose dans cette direction en vérifiant seulement jusqu'à n quot 2 .

3. Jetez la liste génération complètement et l'utilisation de maths pour obtenir un plus rapidement solution. C'est la manière habituelle pour projet Euler problèmes.

( * ) j'ai obtenu ceci en mettant votre code dans un fichier appelé eu13.hs , en ajoutant une fonction principale main = print $ sol 90 . Puis lancer visual-prof -px eu13.hs eu13 et le résultat est eu13.hs.html .

57
répondu Daniel Velkov 2014-11-23 19:33:15

Haskell note: triaList2 est bien sûr plus rapide que le triaList parce que celui-ci effectue beaucoup de calculs inutiles. Il faudra du temps quadratique pour calculer n premiers éléments de triaList , mais linéaire pour triaList2 . Il existe une autre façon élégante (et efficace) de définir une liste infinie de numéros de triangle paresseux:

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

Mathématiques liée remarque: il n'est pas nécessaire de vérifier tous les diviseurs de n / 2, il suffit de vérifier jusqu'à sqrt (n).

3
répondu rkhayrov 2010-07-18 16:54:24

vous pouvez exécuter votre programme avec des options pour activer le profilage temporel. Quelque chose comme ceci:

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

qui devrait exécuter le programme et produire un fichier appelé programme.des statistiques qui indiqueront le temps consacré à chaque fonction. Vous pouvez trouver plus d'informations sur le profilage avec GHC dans le GHC guide de l'utilisateur . Pour le benchmarking, il y a la bibliothèque de critères. J'ai trouvé ce blog a une introduction utile.

1
répondu user394827 2010-07-18 17:56:04