làm thế nào để tìm ra lý do tại sao giải pháp này rất chậm. Có lệnh nào cho tôi biết phần lớn thời gian tính toán được sử dụng ở đâu để tôi biết phần nào trong chương trình băm của mình chậm không?
Đúng! GHC cung cấp nhiều công cụ tuyệt vời, bao gồm:
Hướng dẫn sử dụng cấu hình thời gian và không gian là một phần của Real World Haskell .
Thống kê GC
Đầu tiên, hãy đảm bảo rằng bạn đang biên dịch với ghc -O2. Và bạn có thể chắc chắn rằng đó là GHC hiện đại (ví dụ: GHC 6.12.x)
Điều đầu tiên chúng ta có thể làm là kiểm tra xem việc thu gom rác có phải là vấn đề không. Chạy chương trình của bạn với + 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
Điều này đã cung cấp cho chúng tôi rất nhiều thông tin: bạn chỉ có 2 triệu heap và GC chiếm 0,8% thời gian. Vì vậy, không cần phải lo lắng rằng phân bổ là vấn đề.
Hồ sơ thời gian
Nhận được một hồ sơ thời gian cho chương trình của bạn là ngay lập tức: biên dịch với -prof -auto-all
$ ghc -O2 --make A.hs -prof -auto-all
[1 of 1] Compiling Main ( A.hs, A.o )
Linking A ...
Và, đối với N = 200:
$ time ./A +RTS -p
749700
./A +RTS -p 13.23s user 0.06s system 98% cpu 13.547 total
tạo một tệp, A.prof, chứa:
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
Cho biết rằng tất cả thời gian của bạn được dành cho numDivs và nó cũng là nguồn của tất cả các phân bổ của bạn.
Hồ sơ đống
Bạn cũng có thể chia nhỏ các phân bổ đó bằng cách chạy với + RTS -p -hy, tạo A.hp, mà bạn có thể xem bằng cách chuyển đổi nó thành tệp tái bút (hp2ps -c A.hp), tạo:
điều này cho chúng tôi biết không có gì sai trong việc sử dụng bộ nhớ của bạn: nó đang phân bổ trong không gian cố định.
Vì vậy, vấn đề của bạn là độ phức tạp thuật toán của numDivs:
toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2
Khắc phục điều đó, đó là 100% thời gian chạy của bạn và mọi thứ khác đều dễ dàng.
Tối ưu hóa
Biểu thức này là một ứng cử viên tốt cho tối ưu hóa tổng hợp luồng , vì vậy tôi sẽ viết lại nó để sử dụng Data.Vector , như sau:
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))
Điều này sẽ kết hợp thành một vòng lặp duy nhất mà không có phân bổ heap không cần thiết. Nghĩa là, nó sẽ có độ phức tạp tốt hơn (bởi các yếu tố không đổi) so với phiên bản danh sách. Bạn có thể sử dụng công cụ ghc-core (dành cho người dùng nâng cao) để kiểm tra mã trung gian sau khi tối ưu hóa.
Kiểm tra điều này, ghc -O2 --make Z.hs
$ time ./Z
749700
./Z 3.73s user 0.01s system 99% cpu 3.753 total
Vì vậy, nó đã giảm thời gian chạy cho N = 150 đi 3,5 lần mà không thay đổi chính thuật toán.
Phần kết luận
Vấn đề của bạn là numDivs. Đó là 100% thời gian chạy của bạn và có độ phức tạp khủng khiếp. Hãy nghĩ về numDivs, và ví dụ, với mỗi N bạn đang tạo [2 .. n div
2 + 1] N lần như thế nào. Hãy thử ghi nhớ điều đó, vì các giá trị không thay đổi.
Để đo lường chức năng nào của bạn nhanh hơn, hãy xem xét sử dụng tiêu chí , tiêu chí này sẽ cung cấp thông tin thống kê mạnh mẽ về những cải thiện dưới micro giây trong thời gian chạy.
Addenda
Vì numDivs chiếm 100% thời gian chạy của bạn, nên việc chạm vào các phần khác của chương trình sẽ không tạo ra nhiều khác biệt, tuy nhiên, vì mục đích sư phạm, chúng tôi cũng có thể viết lại những phần đó bằng cách sử dụng kết hợp luồng.
Chúng tôi cũng có thể viết lại trialList và dựa vào sự hợp nhất để biến nó thành vòng lặp mà bạn viết bằng tay trong trialList2, là một chức năng "quét tiền tố" (hay còn gọi là scanl):
triaList = U.scanl (+) 0 (U.enumFrom 1 top)
where
top = 10^6
Tương tự cho sol:
sol :: Int -> Int
sol n = U.head $ U.filter (\x -> numDivs x > n) triaList
Với thời gian chạy tổng thể giống nhau, nhưng mã sạch hơn một chút.
time
Tiện ích mà Don đề cập trong Hồ sơ thời gian chỉ làtime
chương trình Linux . Nó không có sẵn trong Windows. Vì vậy, để lập hồ sơ thời gian trên Windows (thực tế ở bất kỳ đâu), hãy xem câu hỏi này .