come scoprire perché questa soluzione è così lenta. Ci sono comandi che mi dicono dove trascorre la maggior parte del tempo di calcolo, quindi so quale parte del mio programma haskell è lenta?
Precisamente! GHC fornisce molti strumenti eccellenti, tra cui:
Un tutorial sull'uso del tempo e la profilazione dello spazio è part of Real World Haskell.
GC Statistiche
In primo luogo, assicurarsi che si sta compilando con -O2 GHC. E potresti assicurarti che sia un GHC moderno (ad esempio GHC 6.12.x)
La prima cosa che possiamo fare è controllare che la garbage collection non sia il problema. Eseguire il programma con + 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
che già ci dà un sacco di informazioni: si ha solo un mucchio 2M, e GC occupa lo 0,8% del tempo. Quindi non c'è bisogno di preoccuparsi che l'allocazione sia il problema.
Tempo Profili
Ottenere un profilo temporale per il vostro programma è semplice: compilare con -Prof -auto-all
$ ghc -O2 --make A.hs -prof -auto-all
[1 of 1] Compiling Main (A.hs, A.o)
Linking A ...
E, per N = 200:
$ time ./A +RTS -p
749700
./A +RTS -p 13.23s user 0.06s system 98% cpu 13.547 total
che crea un file, A.prof, contenente:
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
Indicando che all il tuo tempo è trascorso in numDivs, ed è anche la fonte di tutte le tue allocazioni.
Profili Heap
È inoltre possibile ottenere una ripartizione di queste allocazioni, eseguendo con + RTS -hy -p, che crea A.hp, che è possibile visualizzare convertendolo in un file PostScript (hp2ps -c A.hp), generando:
che ci dice non c'è niente di sbagliato con l'utilizzo di memoria: è l'allocazione nello spazio costante.
Così il vostro problema è la complessità algoritmica di numDivs:
toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2
correzione, che è il 100% del il tempo di esecuzione, e tutto il resto è facile.
Ottimizzazioni
questa espressione è un buon candidato per la stream fusion ottimizzazione, quindi mi riscriverlo utilizzare Data.Vector, in questo modo:
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))
Che dovrebbe fondersi in un unico ciclo senza allocazioni di heap non necessarie. Cioè, avrà una migliore complessità (da fattori costanti) rispetto alla versione lista. È possibile utilizzare lo strumento ghc-core (per utenti esperti) per ispezionare il codice intermedio dopo l'ottimizzazione.
Testare questo, ghc -O2 --make Z.hs
$ time ./Z
749700
./Z 3.73s user 0.01s system 99% cpu 3.753 total
Quindi ha ridotto il tempo di esecuzione per N = 150 di 3,5x, senza modificare l'algoritmo stesso.
Conclusione
Il tuo problema è numDivs. È il 100% del tempo di esecuzione e ha una complessità terribile. Pensa a numDivs e come, ad esempio, per ogni N stai generando [2 .. n div
2 + 1] N volte. Prova a memoizzarlo, poiché i valori non cambiano.
Per misurare quale delle funzioni è più veloce, considerare l'utilizzo di criterion, che fornirà informazioni statisticamente valide sui miglioramenti del microsecondo in termini di tempo di esecuzione.
Addenda
Dal numDivs è al 100% del il tempo di esecuzione, toccando altre parti del programma non farà molta differenza, tuttavia, a fini pedagogici, possiamo anche riscrivere quelli che utilizzano fusione di corrente.
Possiamo anche riscrivere trialList, e si basano sulla fusione per trasformarlo in loop si scrive a mano in trialList2, che è una funzione "scan prefisso" (aka scanl):
triaList = U.scanl (+) 0 (U.enumFrom 1 top)
where
top = 10^6
Allo stesso modo per sol:
sol :: Int -> Int
sol n = U.head $ U.filter (\x -> numDivs x > n) triaList
Con lo stesso tempo di esecuzione complessivo, ma un codice un po 'più pulito.
Solo una nota per altri idioti come me: L'utilità 'time' che Don ha citato in Time Profile è solo il programma' time' di Linux. Non è disponibile in Windows. Quindi per il profiling temporale su Windows (ovunque ci sia effettivamente), vedi [this] (http://stackoverflow.com/questions/5968614/how-to-get-a-programs-running-time-in-haskell) domanda. –