2010-10-16 25 views
100

Tôi không thể hiểu tại sao m1 được rõ ràng memoized khi m2 không nằm trong những điều sau đây:Khi nào tính năng ghi nhớ tự động trong GHC Haskell?

m1  = ((filter odd [1..]) !!) 

m2 n = ((filter odd [1..]) !! n) 

m1 10000000 mất khoảng 1,5 giây trên cuộc gọi đầu tiên, và một phần nhỏ của chương trình trên các cuộc gọi tiếp theo (có lẽ nó lưu trữ danh sách), trong khi m2 10000000 luôn mất cùng một lượng thời gian (xây dựng lại danh sách với mỗi cuộc gọi). Có ai biết cái gì đang xảy ra không? Có bất kỳ quy tắc nào về việc liệu khi nào và khi nào GHC sẽ ghi nhớ một chức năng? Cảm ơn.

Trả lời

105

GHC không ghi nhớ chức năng.

Tuy nhiên, nó tính toán bất kỳ biểu thức đã cho nào trong mã một lần mỗi lần biểu thức lambda xung quanh của nó được nhập hoặc tối đa một lần nếu nó ở cấp cao nhất. Xác định nơi lambda-biểu thức là có thể là một chút khó khăn khi bạn sử dụng cú pháp đường như trong ví dụ của bạn, vì vậy hãy chuyển đổi các cú pháp khử đường tương đương:

m1' = (!!) (filter odd [1..])    -- NB: See below! 
m2' = \n -> (!!) (filter odd [1..]) n 

(Lưu ý: Báo cáo Haskell 98 thực sự mô tả một nhà khai thác trái phần như (a %) như tương đương với \b -> (%) a b, nhưng GHC desugars nó để (%) a. đây là những kỹ thuật khác nhau vì chúng có thể được phân biệt bởi seq. tôi nghĩ rằng tôi có thể đã đệ trình một vé GHC Trác về việc này.)

vì điều này, bạn có thể thấy rằng trong m1', biểu thức filter odd [1..] không được chứa i n bất kỳ biểu thức lambda nào, do đó, nó sẽ chỉ được tính một lần cho mỗi lần chạy chương trình của bạn, trong khi ở m2', filter odd [1..] sẽ được tính mỗi lần biểu thức lambda được nhập, tức là mỗi cuộc gọi của m2'. Điều đó giải thích sự khác biệt về thời gian bạn đang thấy.


Thực tế, một số phiên bản của GHC, với các tùy chọn tối ưu nhất định, sẽ chia sẻ nhiều giá trị hơn mô tả ở trên cho biết. Điều này có thể có vấn đề trong một số trường hợp. Ví dụ, hãy xem xét các chức năng

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x]) 

GHC thể nhận thấy rằng y không phụ thuộc vào x và viết lại các chức năng để

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x]) 

Trong trường hợp này, phiên bản mới là rất ít hiệu quả vì nó sẽ có để đọc khoảng 1 GB từ bộ nhớ trong đó y được lưu trữ, trong khi phiên bản gốc sẽ chạy trong không gian cố định và phù hợp với bộ nhớ cache của bộ xử lý. Thực tế, theo GHC 6.12.1, hàm f nhanh gấp hai lần khi biên dịch mà không cần tối ưu hóa so với được biên dịch bằng -O2.

+0

Chi phí để đánh giá (lọc lẻ [1 ..]) biểu thức gần bằng không anyway - đó là danh sách lười biếng sau khi tất cả, vì vậy chi phí thực tế là trong (x !! 10000000) ứng dụng khi danh sách thực sự được đánh giá. Bên cạnh đó, cả m1 và m2 dường như chỉ được đánh giá một lần với -O2 và -O1 (trên ghc của tôi 6.12.3) ít nhất là trong thử nghiệm sau đây: (test = m1 10000000 'seq' m1 10000000). Có một sự khác biệt mặc dù không có cờ tối ưu nào được chỉ định. Và cả hai biến thể của "f" của bạn đều có tối đa 5356 byte bất kể tối ưu hóa, bằng cách này (với tổng phân bổ ít hơn khi sử dụng -O2). –

+1

@ Ed'ka: Hãy thử chương trình thử nghiệm này, với định nghĩa ở trên là 'f':' main = tương tác $ unlines. (hiển thị. bản đồ f. đọc). dòng'; biên dịch có hoặc không có '-O2'; sau đó 'echo 1 | ./main'. Nếu bạn viết một bài kiểm tra như 'main = print (f 5)', thì 'y' có thể là rác được thu thập khi nó được sử dụng và không có sự khác biệt giữa hai' f '. –

+0

er, tất nhiên phải là 'map (show. F. Read)'. Và bây giờ tôi đã tải xuống GHC 6.12.3, tôi thấy kết quả tương tự như trong GHC 6.12.1. Và có, bạn là đúng về bản gốc 'm1' và' m2': phiên bản của GHC thực hiện loại nâng này với tối ưu hóa được kích hoạt sẽ biến 'm2' thành' m1'. –

26

m1 chỉ được tính một lần vì đó là Biểu mẫu áp dụng liên tục, trong khi m2 không phải là CAF và do đó được tính cho mỗi đánh giá.

Xem wiki GHC trên CAFS: http://www.haskell.org/haskellwiki/Constant_applicative_form

+0

Giải thích “m1 chỉ được tính một lần bởi vì nó là một dạng áp dụng liên tục” không có ý nghĩa với tôi. Bởi vì có lẽ cả m1 và m2 là các biến cấp cao nhất, tôi nghĩ rằng các hàm _functions_ này chỉ được tính một lần, cho dù chúng là CAF hay không. Sự khác biệt là liệu danh sách '[1 ..]' chỉ được tính một lần trong khi thực thi một chương trình hay nó được tính một lần cho mỗi ứng dụng của hàm, nhưng nó có liên quan đến CAF không? –

+1

Từ trang được liên kết: "CAF ... có thể được biên dịch thành một phần biểu đồ sẽ được chia sẻ bởi tất cả các công dụng hoặc một số mã được chia sẻ sẽ tự ghi đè lên bằng một số đồ thị lần đầu tiên được đánh giá". Vì 'm1' là một CAF, thứ hai được áp dụng và' filter odd [1 ..] '(không chỉ' [1 ..] '!) Chỉ được tính một lần. GHC cũng có thể lưu ý rằng 'm2' đề cập đến' filter odd [1 ..] ', và đặt một liên kết đến cùng một thunk được sử dụng trong' m1', nhưng đó sẽ là một ý tưởng tồi: nó có thể dẫn đến rò rỉ bộ nhớ lớn trong một số tình huống. –

+0

@Alexey: Cảm ơn bạn đã sửa đổi về '[1 ..]' và 'lọc lẻ [1 ..]'. Đối với phần còn lại, tôi vẫn không thuyết phục. Nếu tôi không nhầm, CAF chỉ liên quan khi chúng ta muốn lập luận rằng trình biên dịch _could_ thay thế 'bộ lọc lẻ [1 ..] 'in' m2' bởi một thunk toàn cầu (có thể thậm chí là đoạn giống như đoạn được sử dụng trong 'm1'). Nhưng trong tình huống của người hỏi, trình biên dịch đã làm _not_ làm điều đó "tối ưu hóa", và tôi không thể thấy sự liên quan của nó với câu hỏi. –

13

Có một sự khác biệt quan trọng giữa hai hình thức: những hạn chế monomorphism áp dụng cho m1 nhưng không m2, vì m2 đã đưa ra một cách rõ ràng đối số. Vì vậy, loại m2 là chung nhưng m1 là cụ thể.Các loại họ được giao nhiệm vụ là:

m1 :: Int -> Integer 
m2 :: (Integral a) => Int -> a 

trình biên dịch Hầu hết Haskell và phiên dịch (tất cả trong số họ mà tôi biết trên thực tế) không memoize cấu trúc đa hình, vì vậy danh sách nội m2 được tái tạo mỗi lần nó được gọi là, nơi của m1 không phải là .

+1

Chơi với những thứ này trong GHCi, có vẻ như nó cũng phụ thuộc vào phép biến đổi thả nổi (một trong những tối ưu hóa của GHC mà không được sử dụng trong GHCi). Và dĩ nhiên khi biên dịch các hàm đơn giản này, trình tối ưu hóa có thể làm cho chúng hoạt động giống hệt nhau (theo một số bài kiểm tra tiêu chí mà tôi đã chạy, với các hàm trong một mô-đun riêng biệt và được đánh dấu bằng các pragma NOINLINE). Có lẽ đó là bởi vì việc tạo danh sách và lập chỉ mục được hợp nhất thành một vòng lặp siêu chặt chẽ. – mokus

1

Tôi không chắc chắn, bởi vì tôi khá mới đối với Haskell, nhưng có vẻ như việc sử dụng chức năng thứ hai là không quan trọng và điều đầu tiên là không. Bản chất của hàm là, nó là kết quả phụ thuộc vào giá trị đầu vào và trong mô hình chức năng especailly nó phụ thuộc chỉ vào đầu vào. Rõ ràng ngụ ý là một hàm không có tham số trả về luôn luôn có cùng một giá trị hơn và hơn, không có vấn đề gì.

Có một cơ chế tối ưu hóa trong trình biên dịch GHC, khai thác thực tế này để tính toán giá trị của một hàm như vậy chỉ một lần cho toàn bộ thời gian chạy chương trình. Nó làm nó uể oải, để chắc chắn, nhưng dù sao đi nữa. Tôi tự nhận thấy, khi tôi viết chức năng sau:

primes = filter isPrime [2..] 
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n] 
     where f `divides` n = (n `mod` f) == 0 

Sau đó, để kiểm tra, tôi đã nhập GHCI và viết: primes !! 1000. Phải mất một vài giây, nhưng cuối cùng tôi đã nhận được câu trả lời: 7927. Sau đó, tôi gọi là primes !! 1001 và nhận câu trả lời ngay lập tức. Tương tự như vậy ngay lập tức tôi đã nhận được kết quả cho take 1000 primes, bởi vì Haskell đã phải tính toán toàn bộ danh sách nghìn phần tử để trả về phần tử 1001st trước đây.

Vì vậy, nếu bạn có thể viết hàm của mình sao cho không có tham số, bạn có thể muốn nó. ;)

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