bagaimana mencari tahu mengapa solusi ini sangat lambat. Apakah ada perintah yang memberi tahu saya di mana sebagian besar waktu komputasi dihabiskan sehingga saya tahu bagian mana dari program haskell saya yang lambat?
Tepat! GHC menyediakan banyak alat yang sangat baik, termasuk:
Tutorial tentang penggunaan profil ruang dan waktu adalah bagian dari Real World Haskell .
Statistik GC
Pertama, pastikan Anda melakukan kompilasi dengan ghc -O2. Dan Anda mungkin memastikan ini adalah GHC modern (misalnya GHC 6.12.x)
Hal pertama yang dapat kita lakukan adalah memeriksa bahwa pengumpulan sampah bukanlah masalahnya. Jalankan program Anda dengan + 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
Yang sudah memberi kita banyak informasi: Anda hanya memiliki tumpukan 2 juta, dan GC membutuhkan waktu 0,8%. Jadi tidak perlu khawatir alokasi itu masalahnya.
Profil Waktu
Mendapatkan profil waktu untuk program Anda sangatlah mudah: kompilasi dengan -prof -auto-all
$ ghc -O2 --make A.hs -prof -auto-all
[1 of 1] Compiling Main ( A.hs, A.o )
Linking A ...
Dan, untuk N = 200:
$ time ./A +RTS -p
749700
./A +RTS -p 13.23s user 0.06s system 98% cpu 13.547 total
yang membuat file, A.prof, berisi:
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
Menunjukkan bahwa semua waktu Anda dihabiskan di numDivs, dan ini juga merupakan sumber dari semua alokasi Anda.
Profil Heap
Anda juga bisa mendapatkan rincian alokasi tersebut, dengan menjalankan + RTS -p -hy, yang membuat A.hp, yang dapat Anda lihat dengan mengonversinya menjadi file postscript (hp2ps -c A.hp), menghasilkan:
yang memberi tahu kita bahwa tidak ada yang salah dengan penggunaan memori Anda: ia mengalokasikan dalam ruang yang konstan.
Jadi masalah Anda adalah kompleksitas algoritmik numDivs:
toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2
Perbaiki itu, yang 100% dari waktu berjalan Anda, dan yang lainnya mudah.
Optimasi
Ekspresi ini adalah kandidat yang baik untuk pengoptimalan fusi aliran , jadi saya akan menulis ulang untuk menggunakan Data.Vector , seperti ini:
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))
Yang harus digabungkan menjadi satu loop tanpa alokasi heap yang tidak perlu. Artinya, versi tersebut akan memiliki kompleksitas yang lebih baik (berdasarkan faktor konstan) daripada versi daftar. Anda dapat menggunakan alat ghc-core (untuk pengguna tingkat lanjut) untuk memeriksa kode perantara setelah pengoptimalan.
Menguji ini, ghc -O2 --make Z.hs
$ time ./Z
749700
./Z 3.73s user 0.01s system 99% cpu 3.753 total
Jadi itu mengurangi waktu berjalan untuk N = 150 sebesar 3,5x, tanpa mengubah algoritme itu sendiri.
Kesimpulan
Masalah Anda adalah numDivs. Ini adalah 100% dari waktu berjalan Anda, dan memiliki kerumitan yang mengerikan. Pikirkan tentang numDivs, dan bagaimana, misalnya, untuk setiap N Anda menghasilkan [2 .. n div
2 + 1] N kali. Cobalah mengingatnya, karena nilainya tidak berubah.
Untuk mengukur fungsi mana yang lebih cepat, pertimbangkan untuk menggunakan kriteria , yang akan memberikan informasi yang kuat secara statistik tentang peningkatan sub-mikrodetik dalam waktu berjalan.
Tambahan
Karena numDivs adalah 100% waktu berjalan Anda, menyentuh bagian lain dari program tidak akan membuat banyak perbedaan, namun, untuk tujuan pedagogis, kami juga dapat menulis ulang menggunakan fusi aliran.
Kami juga dapat menulis ulang trialList, dan mengandalkan fusion untuk mengubahnya menjadi loop yang Anda tulis dengan tangan di trialList2, yang merupakan fungsi "pemindaian awalan" (alias scanl):
triaList = U.scanl (+) 0 (U.enumFrom 1 top)
where
top = 10^6
Demikian pula untuk sol:
sol :: Int -> Int
sol n = U.head $ U.filter (\x -> numDivs x > n) triaList
Dengan waktu berjalan keseluruhan yang sama, tetapi kode yang sedikit lebih bersih.
time
Utilitas yang disebutkan Don di Profil Waktu hanyalahtime
program Linux . Ini tidak tersedia di Windows. Jadi untuk pembuatan profil waktu di Windows (di mana saja sebenarnya), lihat pertanyaan ini .