Các công cụ để phân tích hiệu suất của một chương trình Haskell


104

Trong khi giải quyết một số Vấn đề của Project Euler để học Haskell (vì vậy hiện tại tôi là người mới bắt đầu hoàn toàn), tôi đã xem qua Vấn đề 12 . Tôi đã viết giải pháp (ngây thơ) này:

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

Giải pháp này cho n=500 (sol 500)là cực kỳ chậm (chạy hơn 2 giờ bây giờ), vì vậy tôi tự hỏi 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 haskell của mình bị chậm không? Một cái gì đó giống như một hồ sơ đơn giản.

Để làm cho nó rõ ràng, Tôi không yêu cầu cho một giải pháp nhanh hơn nhưng đối với một cách để tìm giải pháp này. Bạn sẽ bắt đầu như thế nào nếu bạn không có kiến ​​thức về haskell?

Tôi đã cố gắng viết hai triaListhàm nhưng không tìm thấy cách nào để kiểm tra cái nào nhanh hơn, vì vậy đây là nơi mà vấn đề của tôi bắt đầu.

Cảm ơn

Câu trả lời:


187

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:

văn bản thay thế

đ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 div2 + 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.


Chỉ một lưu ý cho những kẻ ngốc khác như tôi: timeTiện ích mà Don đề cập trong Hồ sơ thời gian chỉ là timechươ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 .
John Red,

1
Đối với những người dùng trong tương lai, -auto-allkhông được dùng để ủng hộ -fprof-auto.
B. Mehta

60

Câu trả lời của Dons là tuyệt vời mà không phải là một kẻ ngụy biện bằng cách đưa ra một giải pháp trực tiếp cho vấn đề.
Ở đây tôi muốn đề xuất một công cụ nhỏ mà tôi đã viết gần đây. Nó giúp bạn tiết kiệm thời gian để viết các chú thích SCC bằng tay khi bạn muốn có một hồ sơ chi tiết hơn so với mặc định ghc -prof -auto-all. Bên cạnh đó nó đầy màu sắc!

Đây là một ví dụ với mã bạn đã cung cấp (*), màu xanh lục là OK, màu đỏ là chậm: văn bản thay thế

Tất cả thời gian dành cho việc tạo danh sách các ước số. Điều này gợi ý một số điều bạn có thể làm:
1. Làm cho bộ lọc n rem x == 0nhanh hơn, nhưng vì nó là một chức năng tích hợp nên có thể nó đã nhanh rồi.
2. Tạo một danh sách ngắn hơn. Bạn đã làm điều gì đó theo hướng đó bằng cách chỉ kiểm tra tối đa n quot 2.
3. Loại bỏ hoàn toàn việc tạo danh sách và sử dụng một số phép toán để có được một giải pháp nhanh hơn. Đây là cách thông thường cho các vấn đề của dự án Euler.

(*) Tôi hiểu được điều này bằng cách đưa mã của bạn vào một tệp có tên eu13.hs, thêm một hàm chính main = print $ sol 90. Sau đó chạy visual-prof -px eu13.hs eu13và kết quả là trong eu13.hs.html.


3

Ghi chú liên quan đến Haskell: triaList2tất nhiên là nhanh hơn triaListvì cái sau thực hiện rất nhiều phép tính không cần thiết. Sẽ mất thời gian bậc hai để tính n phần tử đầu tiên của triaList, nhưng tuyến tính cho triaList2. Có một cách thanh lịch (và hiệu quả) khác để xác định danh sách vô hạn các số tam giác:

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

Lưu ý liên quan đến toán học: không cần phải kiểm tra tất cả các ước số đến n / 2, chỉ cần kiểm tra đến sqrt (n) là đủ.


2
Ngoài ra xem xét: scanl (+) 1 [2 ..]
Don Stewart

1

Bạn có thể chạy chương trình của mình với các cờ để kích hoạt cấu hình thời gian. Một cái gì đó như thế này:

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

Điều đó sẽ chạy chương trình và tạo ra một tệp có tên là program.stats sẽ có bao nhiêu thời gian được sử dụng cho mỗi hàm. Bạn có thể tìm thêm thông tin về việc lập hồ sơ với GHC trong hướng dẫn sử dụng GHC . Đối với điểm chuẩn, có thư viện Tiêu chí. Tôi đã tìm thấy này bài đăng blog có một giới thiệu hữu ích.


1
Nhưng trước tiên biên dịch nó vớighc -prof -auto-all -fforce-recomp --make -O2 program.hs
Daniel
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.