2011-12-28 49 views
18

Cảnh báo cảnh báo: liên quan đến Problem 14 từ Project Euler.Tại sao thuật toán haskell đơn giản này lại chậm?

Mã sau mất khoảng 15 giây để chạy. Tôi có một giải pháp Java không đệ quy chạy trong 1s. Tôi nghĩ rằng tôi sẽ có thể nhận được mã này gần gũi hơn với điều đó.

import Data.List 

collatz a 1 = a 
collatz a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

main = do 
    print ((foldl1' max) . map (collatz 1) $ [1..1000000]) 

Tôi đã lược tả với +RHS -p và nhận thấy rằng bộ nhớ được phân bổ lớn và phát triển khi đầu vào tăng. Đối với n = 100,000 1gb được cấp phát (!), Cho n = 1,000,000 13gb (!!) được phân bổ.

Sau đó, một lần nữa, -sstderr cho thấy mặc dù nhiều byte được phân bổ, tổng dung lượng bộ nhớ là 1mb và năng suất là 95% +, vì vậy có thể 13gb là cá trích đỏ.

tôi có thể nghĩ ra một vài khả năng:

  1. Something là không chặt chẽ như nó cần phải được. Tôi đã phát hiện foldl1', nhưng có lẽ tôi cần phải làm nhiều hơn? Có thể đánh dấu collatz như nghiêm ngặt (Điều đó thậm chí có ý nghĩa?

  2. collatz không đuôi-gọi tối ưu hóa. Tôi nghĩ rằng nó phải được nhưng đừng biết một cách để xác nhận.

  3. Trình biên dịch không được thực hiện một số tối ưu hóa tôi nghĩ rằng nó nên - ví dụ chỉ có hai kết quả collatz cần phải được trong bộ nhớ cùng một lúc (tối đa và hiện tại)

mọi góp ý

?

Đây là khá nhiều bản sao của Why is this Haskell expression so slow?, mặc dù tôi sẽ lưu ý rằng giải pháp Java nhanh không phải thực hiện bất kỳ ghi nhớ nào. Có cách nào để tăng tốc độ này mà không cần phải sử dụng nó?

Để tham khảo, đây là đầu ra của tôi profiling:

Wed Dec 28 09:33 2011 Time and Allocation Profiling Report (Final) 

    scratch +RTS -p -hc -RTS 

    total time =  5.12 secs (256 ticks @ 20 ms) 
    total alloc = 13,229,705,716 bytes (excludes profiling overheads) 

COST CENTRE     MODULE    %time %alloc 

collatz      Main     99.6 99.4 


                           individual inherited 
COST CENTRE    MODULE            no. entries %time %alloc %time %alloc 

MAIN      MAIN             1   0 0.0 0.0 100.0 100.0 
CAF      Main             208   10 0.0 0.0 100.0 100.0 
    collatz    Main             215   1 0.0 0.0  0.0 0.0 
    main     Main             214   1 0.4 0.6 100.0 100.0 
    collatz    Main             216   0 99.6 99.4 99.6 99.4 
CAF      GHC.IO.Handle.FD          145   2 0.0 0.0  0.0 0.0 
CAF      System.Posix.Internals        144   1 0.0 0.0  0.0 0.0 
CAF      GHC.Conc            128   1 0.0 0.0  0.0 0.0 
CAF      GHC.IO.Handle.Internals        119   1 0.0 0.0  0.0 0.0 
CAF      GHC.IO.Encoding.Iconv        113   5 0.0 0.0  0.0 0.0 

Và -sstderr:

./scratch +RTS -sstderr 
525 
    21,085,474,908 bytes allocated in the heap 
     87,799,504 bytes copied during GC 
      9,420 bytes maximum residency (1 sample(s))   
      12,824 bytes maximum slop    
       1 MB total memory in use (0 MB lost due to fragmentation) 

    Generation 0: 40219 collections,  0 parallel, 0.40s, 0.51s elapsed 
    Generation 1:  1 collections,  0 parallel, 0.00s, 0.00s elapsed 

    INIT time 0.00s ( 0.00s elapsed) 
    MUT time 35.38s (36.37s elapsed) 
    GC time 0.40s ( 0.51s elapsed) 
    RP time 0.00s ( 0.00s elapsed) PROF time 0.00s ( 0.00s elapsed) 
    EXIT time 0.00s ( 0.00s elapsed) 
    Total time 35.79s (36.88s elapsed) %GC time  1.1% (1.4% elapsed) Alloc rate 595,897,095 bytes per MUT second 

    Productivity 98.9% of total user, 95.9% of total elapsed 

Và giải pháp Java (không phải của tôi, lấy từ các diễn đàn Project Euler với memoization loại bỏ):

public class Collatz { 
    public int getChainLength(int n) 
    { 
    long num = n; 
    int count = 1; 
    while(num > 1) 
    { 
     num = (num%2 == 0) ? num >> 1 : 3*num+1; 
     count++; 
    } 
    return count; 
    } 

    public static void main(String[] args) { 
    Collatz obj = new Collatz(); 
    long tic = System.currentTimeMillis(); 
    int max = 0, len = 0, index = 0; 
    for(int i = 3; i < 1000000; i++) 
    { 
     len = obj.getChainLength(i); 
     if(len > max) 
     { 
     max = len; 
     index = i; 
     } 
    } 
    long toc = System.currentTimeMillis(); 
    System.out.println(toc-tic); 
    System.out.println("Index: " + index + ", length = " + max); 
    } 
} 
+0

Thật đáng ngạc nhiên rằng GHC không tối ưu hóa (quot n 2) đến (rshift n 1) giống như bất kỳ trình biên dịch C tự tôn trọng nào được mong đợi sẽ làm. Có lý do gì không? –

+0

@ solrize: Nó làm tôi ngạc nhiên. – ehird

Trả lời

20

Lúc đầu, tôi nghĩ bạn nên thử đặt dấu chấm than trước một trong collatz:

collatz !a 1 = a 
collatz !a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

(Bạn sẽ cần phải đặt {-# LANGUAGE BangPatterns #-} ở phía trên cùng của tập tin nguồn của bạn để làm việc này.)

Lý luận của tôi đi như sau: Vấn đề là bạn xây dựng một số lượng lớn thunk trong đối số đầu tiên cho collatz: nó bắt đầu là 1, và sau đó trở thành 1 + 1, và sau đó trở thành (1 + 1) + 1, ... tất cả mà không bao giờ bị ép buộc.Điều này bang pattern buộc đối số đầu tiên của collatz buộc phải thực hiện bất kỳ khi nào cuộc gọi được thực hiện, vì vậy nó bắt đầu bằng 1 và sau đó trở thành 2 và cứ như vậy, không xây dựng một đoạn lớn chưa được đánh giá cao: nó chỉ ở dạng số nguyên.

Lưu ý rằng mẫu hình chữ nhật chỉ là viết tắt để sử dụng seq; trong trường hợp này, chúng ta có thể viết lại collatz như sau:

collatz a _ | seq a False = undefined 
collatz a 1 = a 
collatz a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

Bí quyết ở đây là để buộc một trong bảo vệ, sau đó luôn luôn đánh giá False (và do đó cơ thể là không thích hợp). Sau đó, đánh giá tiếp tục với trường hợp tiếp theo, a đã được đánh giá. Tuy nhiên, một mô hình bang là rõ ràng hơn.

Thật không may, khi được biên dịch với -O2, điều này không chạy nhanh hơn bản gốc! Chúng ta có thể thử những gì khác? Vâng, có một điều chúng ta có thể làm là giả định rằng hai con số không bao giờ tràn một số nguyên máy có kích thước, và cung cấp cho collatz chú thích kiểu này:

collatz :: Int -> Int -> Int 

Chúng tôi sẽ rời khỏi mô hình vụ nổ ở đó, vì chúng ta vẫn nên tránh xây dựng up thunks, ngay cả khi họ không phải là gốc của vấn đề hiệu suất. Điều này mang lại thời gian xuống còn 8,5 giây trên máy tính (chậm) của tôi.

Bước tiếp theo là thử đưa điều này đến gần hơn với giải pháp Java. Điều đầu tiên để nhận ra là trong Haskell, div hoạt động theo một cách toán học chính xác hơn đối với các số nguyên âm, nhưng chậm hơn phân chia C "bình thường", trong đó Haskell được gọi là quot. Thay thế div bằng quot mang thời gian chạy xuống 5,2 giây và thay thế x `quot` 2 bằng x `shiftR` 1 (nhập Data.Bits) để khớp với giải pháp Java đã giảm xuống còn 4,9 giây.

Đây là khoảng thấp như tôi có thể nhận được nó cho bây giờ, nhưng tôi nghĩ rằng đây là một kết quả khá tốt; vì máy tính của bạn nhanh hơn tôi, nên hy vọng nó thậm chí còn gần gũi hơn với giải pháp Java.

Đây là mã cuối cùng (Tôi đã làm một chút dọn dẹp trên đường):

{-# LANGUAGE BangPatterns #-} 

import Data.Bits 
import Data.List 

collatz :: Int -> Int 
collatz = collatz' 1 
    where collatz' :: Int -> Int -> Int 
     collatz' !a 1 = a 
     collatz' !a x 
      | even x = collatz' (a + 1) (x `shiftR` 1) 
      | otherwise = collatz' (a + 1) (3 * x + 1) 

main :: IO() 
main = print . foldl1' max . map collatz $ [1..1000000] 

Nhìn vào GHC cốt lõi cho chương trình này (với ghc-core), tôi nghĩ rằng đây có lẽ là về như tốt như nó được; vòng lặp collatz sử dụng các số nguyên không được hộp và phần còn lại của chương trình trông OK. Cải tiến duy nhất tôi có thể nghĩ đến sẽ loại bỏ quyền anh từ sự lặp lại map collatz [1..1000000].

Nhân tiện, đừng lo lắng về con số "tổng phân bổ"; đó là tổng bộ nhớ được phân bổ trong suốt thời gian của chương trình và không bao giờ giảm ngay cả khi GC thu hồi bộ nhớ đó. Số liệu của nhiều terabyte là phổ biến.

+0

Cảm ơn, điều đó thực sự hữu ích. Tôi không biết về '-O2', điều đó tạo nên sự khác biệt lớn (giảm thời gian chạy xuống 5 giây). Đã thêm giải pháp Java vào câu hỏi. –

+0

Ồ, tôi cho rằng bạn đã sử dụng '-O2' rồi, vì chương trình đã sửa đổi với mẫu hình chữ nhật chạy trong 16 giây trên máy của tôi :) Tôi sẽ xem xét giải pháp Java của bạn. – ehird

+0

OK, tôi đã cập nhật câu trả lời này với các cải tiến khác. – ehird

2

Bạn có thể mất danh sách và kiểu chữ và vẫn nhận được hiệu suất tương tự bằng cách sử dụng ngăn xếp thay thế.

import Data.List 
import Data.Bits 

coll :: Int -> Int 
coll 0 = 0 
coll 1 = 1 
coll 2 = 2 
coll n = 
    let a = coll (n - 1) 
     collatz a 1 = a 
     collatz a x 
     | even x = collatz (a + 1) (x `shiftR` 1) 
     | otherwise = collatz (a + 1) (3 * x + 1) 
    in max a (collatz 1 n) 


main = do 
    print $ coll 100000 

Một vấn đề với điều này là bạn sẽ phải tăng kích thước ngăn xếp cho đầu vào lớn, như 1_000_000.

update:

Đây là một phiên bản đệ quy đuôi mà không gặp phải vấn đề stack overflow.

import Data.Word 
collatz :: Word -> Word -> (Word, Word) 
collatz a x 
    | x == 1 = (a,x) 
    | even x = collatz (a + 1) (x `quot` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

coll :: Word -> Word 
coll n = collTail 0 n 
    where 
    collTail m 1 = m 
    collTail m n = collTail (max (fst $ collatz 1 n) m) (n-1) 

Lưu ý việc sử dụng Word thay vì Int. Nó tạo ra sự khác biệt về hiệu suất. Bạn vẫn có thể sử dụng các mẫu bang nếu bạn muốn, và điều đó sẽ tăng gấp đôi hiệu suất.

0

Một điều tôi thấy đã tạo ra sự khác biệt đáng ngạc nhiên trong vấn đề này. Tôi bị mắc kẹt với các mối quan hệ lặp lại thẳng chứ không phải gấp, bạn nên tha thứ cho biểu thức, đếm với nó. Viết lại

collatz n = if even n then n `div` 2 else 3 * n + 1 

như

collatz n = case n `divMod` 2 of 
      (n', 0) -> n' 
      _  -> 3 * n + 1 

mất 1.2 giây khỏi thời gian chạy cho chương trình của tôi trên một hệ thống với một Athlon 2,8 GHz II X4 430 CPU. Phiên bản ban đầu của tôi nhanh hơn (2,3 giây sau khi sử dụng divMod):

{-# LANGUAGE BangPatterns #-} 

import Data.List 
import Data.Ord 

collatzChainLen :: Int -> Int 
collatzChainLen n = collatzChainLen' n 1 
    where collatzChainLen' n !l 
      | n == 1 = l 
      | otherwise = collatzChainLen' (collatz n) (l + 1) 

collatz:: Int -> Int 
collatz n = case n `divMod` 2 of 
       (n', 0) -> n' 
       _  -> 3 * n + 1 

pairMap :: (a -> b) -> [a] -> [(a, b)] 
pairMap f xs = [(x, f x) | x <- xs] 

main :: IO() 
main = print $ fst (maximumBy (comparing snd) (pairMap collatzChainLen [1..999999])) 

Phiên bản Haskell độc đáo hơn có thể chạy trong khoảng 9,7 giây (8,5 với divMod); nó giống hệt tiết kiệm cho

collatzChainLen :: Int -> Int 
collatzChainLen n = 1 + (length . takeWhile (/= 1) . (iterate collatz)) n 

Sử dụng Data.List.Stream là nghĩa vụ phải cho phép dòng phản ứng tổng hợp mà có thể làm phiên bản chạy này nhiều như thế với sự tích lũy rõ ràng, nhưng tôi không thể tìm thấy một libghc Ubuntu * gói có Data.List.Stream, vì vậy tôi chưa thể xác minh điều đó.

Các vấn đề liên quan