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 .-auto-all
sudah tidak digunakan lagi untuk mendukung-fprof-auto
.Jawaban Don sangat bagus tanpa menjadi spoiler dengan memberikan solusi langsung untuk masalah tersebut.
Disini saya ingin menyarankan sedikit alat yang saya tulis baru-baru ini. Ini menghemat waktu Anda untuk menulis anotasi SCC secara manual saat Anda menginginkan profil yang lebih detail daripada default
ghc -prof -auto-all
. Selain itu warna-warni!Berikut contoh kode yang Anda berikan (*), hijau tidak masalah, merah lambat:
Sepanjang waktu berjalan dalam membuat daftar pembagi. Ini menyarankan beberapa hal yang dapat Anda lakukan:
1. Percepat pemfilteran
n rem x == 0
, tetapi karena ini adalah fungsi bawaan, mungkin sudah cepat.2. Buat daftar yang lebih pendek. Anda telah melakukan sesuatu ke arah itu dengan memeriksa hanya sampai
n quot 2
.3. Buang pembuatan daftar sepenuhnya dan gunakan beberapa matematika untuk mendapatkan solusi yang lebih cepat. Ini adalah cara yang biasa untuk masalah proyek Euler.
(*) Saya mendapatkan ini dengan meletakkan kode Anda dalam file bernama
eu13.hs
, menambahkan fungsi utamamain = print $ sol 90
. Lalu larivisual-prof -px eu13.hs eu13
dan hasilnya masukeu13.hs.html
.sumber
Catatan terkait Haskell:
triaList2
tentu saja lebih cepat daripadatriaList
karena yang terakhir melakukan banyak perhitungan yang tidak perlu. Ini akan membutuhkan waktu kuadrat untuk menghitung n elemen pertamatriaList
, tapi linier untuktriaList2
. Ada cara lain yang elegan (dan efisien) untuk menentukan daftar nomor segitiga malas yang tak terbatas:Catatan terkait matematika: tidak perlu memeriksa semua pembagi hingga n / 2, cukup memeriksa hingga akar (n).
sumber
Anda dapat menjalankan program Anda dengan tanda untuk mengaktifkan pembuatan profil waktu. Sesuatu seperti ini:
Itu harus menjalankan program dan menghasilkan file bernama program.stats yang akan memiliki berapa banyak waktu yang dihabiskan di setiap fungsi. Anda dapat menemukan informasi lebih lanjut tentang membuat profil dengan GHC di panduan pengguna GHC . Untuk pembandingan, ada pustaka Kriteria. Saya menemukan entri blog ini memiliki pengantar yang berguna.
sumber
ghc -prof -auto-all -fforce-recomp --make -O2 program.hs