2017-01-09 46 views
10

Hai chức năng Haskell bên dưới dường như chỉ khác nhau khi biến chỉ mục là ẩn hoặc rõ ràng nhưng sự khác biệt về hiệu suất là bởi hai đơn vị độ lớn.Tối ưu hóa GHC

Chức năng này mất khoảng 0,03 giây để tính toán mfib 30:

let mfib = (map fib [0..] !!) 
    where 
    fib 0 = 0 
    fib 1 = 1 
    fib x = mfib (x-1) + mfib (x-2) 

Chức năng này mất khoảng 3 giây cho mfib 30:

let mfib i = map fib [0..] !! i 
    where 
    fib 0 = 0 
    fib 1 = 1 
    fib x = mfib (x-1) + mfib (x-2) 

Tôi đoán nó đã làm với GHC inline quy tắc và đã cố gắng thêm pragmas nội dòng/không có dòng để có được hiệu suất phù hợp.

EDIT: Tôi hiểu cách thực hiện tra cứu trong danh sách lười có thể được sử dụng để ghi nhớ chức năng fib và tại sao định nghĩa truyền thống về fib rất chậm. Tôi đã mong đợi việc ghi nhớ để làm việc trong chức năng thứ hai cũng như đầu tiên và không hiểu tại sao nó không phải là.

+1

Điều quan trọng là * ghi nhớ *. Xem [tại đây] (http://stackoverflow.com/questions/11466284/how-is-this-fibonacci-function-memoized). –

Trả lời

12

Sẽ dễ dàng hơn để hiểu những khác biệt này khi xem mã được giải mã, do đó, đây là các phiên bản được giải thích một phần của hai hàm.

let mfib = let fib 0 = 0 
       fib 1 = 1 
       fib x = mfib (x-1) + mfib (x-2) 
      in (!!) (map fib [0..]) 

so

let mfib = \i -> 
       let fib 0 = 0 
        fib 1 = 1 
        fib x = mfib (x-1) + mfib (x-2) 
       in map fib [0..] !! i 

Lưu ý rằng trong chương trình thứ hai, khái niệm map fib [0..] xuất hiện bên \i -> ..., vì vậy nó sẽ (bình thường, mà không cần tối ưu hóa) đánh giá cho mỗi giá trị của i. Xem When is memoization automatic in GHC Haskell?

+0

Tôi đã làm một số thử nghiệm trên repl để cố gắng xác nhận điều này và có vẻ chính xác. Tôi cũng đã xem xét liên kết được cung cấp bởi @ alexey-radkov về ghi nhớ và gợi ý rằng nó phải làm với chức năng đầu tiên là đơn hình và do đó được chia sẻ giữa các cuộc gọi, trong khi thứ hai là đa hình, nhưng tôi không thể xác nhận điều này bằng cách xác định lại 'bản đồ' là đơn nhất. – Mikkel

+0

TL; DR Bởi vì 'map fib [0 ..]' nằm trong lambda nó không được chia sẻ, nhưng rác được thu thập giữa các cuộc gọi (đệ quy). Chính xác? – Mikkel

+0

@ Mikkel phải, và lý do là vì (mặc dù nó không phải trong trường hợp này) nó có thể phụ thuộc vào 'i', và sau đó không thể được chia sẻ. (Và đây thường là một cách hay để xem những gì sẽ được chia sẻ mà không làm điều xấu đầu tiên.) –

7

Không, điều này không liên quan gì đến nội tuyến. Sự khác biệt là mfib = (map fib [0..] !!) không có đối số. Nó vẫn là một chức năng của khóa học, nhưng đánh giá rằng chức năng không trả trước không yêu cầu phải vượt qua bất kỳ đối số. Cụ thể, việc đánh giá mfib này sẽ tạo danh sách fib theo cách có thể sử dụng lại cho tất cả các chỉ mục.

OTOH, mfib i = map fib [0..] !! i nghĩa là toàn bộ khối where sẽ chỉ được xem xét khi bạn thực sự vượt qua đối số i.

Hai chỉ khác nhau nếu bạn đánh giá một hàm nhiều lần lặp đi lặp lại. Thật không may cho phiên bản thứ hai, các chức năng đệ quy của riêng đã gọi nó một lần nữa và một lần nữa! Vì vậy, mfib (x-1) + mfib (x-2) sau đó cần thực hiện toàn bộ công việc của mfib (x-1), và sau đó lại một lần nữa toàn bộ công việc của mfib (x-2). Vì vậy, mfib n mất hơn gấp đôi so với chi phí tính toán của mfib (n-1), do đó mfibO (2 n).

Điều đó vô cùng lãng phí, bởi vì hầu hết các điều khoản trong mfib (x-2) cũng đã có trong mfib (x-1) và chỉ có thể được sử dụng lại. Vâng, đó là chính xác những gì phiên bản đầu tiên của bạn làm, bởi vì nó tính toán danh sách fib một lần và cho tất cả các chỉ số, vì vậy việc đánh giá mfib (x-1) sẽ thực hiện hầu hết công việc mà sau đó có thể được đọc lại bởi mfib (x-2), giảm độ phức tạp cho đa thức.

+2

Một chút giải thích bổ sung cho _why_ khối 'where' được đánh giá lại: đó là vì' i' nằm trong phần đóng của 'where'. Nếu bạn viết nó là 'let mfib = \ i -> map fib [0 ..] !! tôi ở đâu ... 'nó sẽ nhanh như phiên bản được ký hợp đồng eta. Điều đó nói rằng, tôi ngạc nhiên rằng GHC đã không phát hiện ra một cơ hội để áp dụng sự chuyển đổi đầy đủ sự lười biếng và nổi 'fib' bên ngoài chất kết dính. –

+0

@BenjaminHodgson Tôi đã thực sự cố gắng đặt 'i' trong lambda, nhưng nó không có sự khác biệt - ghi nhớ vẫn không" làm việc ". – Mikkel