Ai đó có thể giải thích khái niệm đằng sau sự ghi nhớ của Haskell không?


12

(lưu ý tôi đặt câu hỏi ở đây vì đó là về cơ chế khái niệm của nó, chứ không phải là vấn đề mã hóa)

Tôi đang làm việc trên một chương trình nhỏ, đó là sử dụng một chuỗi các số của Wikipedia, nhưng tôi nhận thấy rằng nếu tôi vượt qua một số nào đó, nó sẽ bị chậm một cách đau đớn, tôi loạng choạng một chút về một kỹ thuật trong Haskell được biết đến như Memoization, họ đã cho thấy mã làm việc như thế này:

-- Traditional implementation of fibonacci, hangs after about 30
slow_fib :: Int -> Integer
slow_fib 0 = 0
slow_fib 1 = 1
slow_fib n = slow_fib (n-2) + slow_fib (n-1)

-- Memorized variant is near instant even after 10000
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

Vì vậy, câu hỏi của tôi cho các bạn là, làm thế nào hoặc đúng hơn tại sao điều này làm việc?

Có phải vì nó bằng cách nào đó quản lý để chạy qua hầu hết danh sách trước khi tính toán bắt kịp? Nhưng nếu haskell lười biếng, thực sự không có bất kỳ tính toán nào cần theo kịp ... Vậy nó hoạt động như thế nào?


1
bạn có thể làm rõ những gì bạn có nghĩa là the calculation catches upgì? BTW, ghi nhớ không đặc trưng cho haskell: en.wikipedia.org/wiki/Memoization
Simon Bergot

xem lời giải thích của tôi dưới câu trả lời của killan
Cà phê điện

2
Yêu câu hỏi của bạn; chỉ là một ghi chú nhanh: Kỹ thuật này được gọi là memo i zation, không phải memo ri zation.
Racheet

Câu trả lời:


11

Chỉ để giải thích các cơ chế đằng sau việc ghi nhớ thực tế,

memo_fib = (map fib [1..] !!)

tạo ra một danh sách "thunks", các tính toán không được đánh giá. Hãy nghĩ về những thứ này giống như những món quà chưa mở, miễn là chúng ta không chạm vào chúng, chúng sẽ không chạy.

Bây giờ một khi chúng tôi đánh giá một thunk, chúng tôi không bao giờ đánh giá nó một lần nữa. Đây thực sự là hình thức đột biến duy nhất trong haskell "bình thường", thunks đột biến một khi được đánh giá để trở thành giá trị cụ thể.

Quay trở lại mã của bạn, bạn đã có một danh sách các thunks và bạn vẫn thực hiện đệ quy cây này, nhưng bạn sử dụng lại danh sách đó và một khi một yếu tố trong danh sách được đánh giá, nó sẽ không bao giờ được tính lại. Vì vậy, chúng tôi tránh sự đệ quy của cây trong hàm sợi ngây thơ.

Là một lưu ý thú vị, điều này đặc biệt nhanh đối với một loạt các số sợi được tính toán vì danh sách đó chỉ được đánh giá một lần, có nghĩa là nếu bạn tính toán memo_fib 10000 hai lần, thì lần thứ hai sẽ là tức thời. Điều này là do Haskell chỉ đánh giá các đối số cho các hàm một lần và bạn đang sử dụng một phần ứng dụng thay vì lambda.

TLDR: Bằng cách lưu trữ các tính toán trong một danh sách, mỗi phần tử của danh sách được ước tính một lần, do đó, mỗi số lượng sợi quang được tính chính xác một lần trong toàn bộ chương trình.

Hình dung:

 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_5]
 -- Evaluating THUNK_5
 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_3 + THUNK_4]
 [THUNK_1, THUNK_2, THUNK_1 + THUNK_2, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 1 + 1, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 2, THUNK_4, 2 + THUNK4]
 [1, 1, 2, 1 + 2, 2 + THUNK_4]
 [1, 1, 2, 3, 2 + 3]
 [1, 1, 2, 3, 5]

Vì vậy, bạn có thể thấy cách đánh giá THUNK_4nhanh hơn rất nhiều vì các biểu hiện con của nó đã được đánh giá.


bạn có thể cung cấp một ví dụ về cách các giá trị trong danh sách ứng xử cho một chuỗi ngắn không? Tôi nghĩ rằng nó có thể thêm vào hình dung về cách nó hoạt động ... Và trong khi sự thật là nếu tôi gọi memo_fibvới cùng một giá trị hai lần, thì lần thứ hai sẽ ngay lập tức, nhưng nếu tôi gọi nó với giá trị 1 cao hơn, thì nó vẫn mất mãi để đánh giá (như nói đi từ 30 đến 31)
Cà phê điện

@ElectricCoffee Đã thêm
Daniel Gratzer

@ElectricCoffee Không, nó sẽ không được đánh giá memo_fib 29memo_fib 30đã được đánh giá, sẽ mất chính xác chừng nào bạn cần thêm hai số đó :) Một khi có gì đó không đúng, nó vẫn bị loại bỏ.
Daniel Gratzer

1
@ElectricCoffee Đệ quy của bạn phải đi qua danh sách, nếu không bạn sẽ không đạt được bất kỳ hiệu suất nào
Daniel Gratzer

2
@ElectricCoffee Có. nhưng phần tử thứ 31 của danh sách không sử dụng các tính toán trong quá khứ, bạn đang ghi nhớ có, nhưng theo một cách khá vô dụng .. Các tính toán được lặp lại không được tính hai lần, nhưng bạn vẫn có đệ quy cây cho mỗi giá trị mới rất, rất chậm
Daniel Gratzer

1

Điểm ghi nhớ là không bao giờ tính toán cùng một chức năng hai lần - điều này cực kỳ hữu ích để tăng tốc các tính toán hoàn toàn là chức năng, tức là không có tác dụng phụ, vì đối với những quy trình này có thể hoàn toàn tự động mà không ảnh hưởng đến tính chính xác. Điều này đặc biệt cần thiết cho các chức năng như fibo, dẫn đến đệ quy cây , tức là nỗ lực theo cấp số nhân, khi được thực hiện một cách ngây thơ. (Đây là một lý do tại sao các số Fibonacci thực sự là một ví dụ rất tệ cho việc dạy đệ quy - gần như tất cả các triển khai demo bạn tìm thấy trong hướng dẫn hoặc sách đều không sử dụng được cho các giá trị đầu vào lớn.)

Nếu bạn theo dõi luồng thực thi, bạn sẽ thấy rằng trong trường hợp thứ hai, giá trị for fib xsẽ luôn khả dụng khi fib x+1được thực thi hệ thống thời gian chạy sẽ có thể chỉ đọc nó từ bộ nhớ thay vì thông qua một cuộc gọi đệ quy khác, trong khi giải pháp đầu tiên cố gắng tính toán giải pháp lớn hơn trước khi có kết quả cho các giá trị nhỏ hơn. Điều này cuối cùng là vì trình vòng lặp [0..n]được đánh giá từ trái sang phải và do đó sẽ bắt đầu bằng 0, trong khi đệ quy trong ví dụ đầu tiên bắt đầu bằng nvà chỉ sau đó hỏi về n-1. Đây là những gì dẫn đến nhiều, nhiều cuộc gọi chức năng trùng lặp không cần thiết.


ồ tôi hiểu ý của nó, tôi chỉ không hiểu nó hoạt động như thế nào, như từ những gì tôi có thể thấy trong mã, là khi bạn viết memorized_fib 20chẳng hạn, bạn thực sự chỉ đang viết map fib [0..] !! 20, nó vẫn cần tính toán toàn bộ phạm vi số lên đến 20, hoặc tôi đang thiếu một cái gì đó ở đây?
Cà phê điện

1
Có, nhưng chỉ một lần cho mỗi số. Việc thực hiện ngây thơ fib 2thường tính toán đến mức nó sẽ khiến đầu óc bạn quay cuồng - hãy tiếp tục, viết ra lông cây gọi chỉ là một giá trị nhỏ như thế nào n==5. Bạn sẽ không bao giờ quên việc ghi nhớ một lần nữa khi bạn đã thấy những gì nó giúp bạn tiết kiệm.
Kilian Foth

@ElectricCoffee: Có, nó sẽ tính toán tỷ lệ từ 1 đến 20. Bạn không nhận được gì từ cuộc gọi đó. Bây giờ hãy thử tính toán 21, và bạn sẽ thấy rằng thay vì tính 1-21, bạn chỉ có thể tính 21 vì bạn đã tính 1-20 và không cần phải làm lại.
Phoshi

Tôi đang cố gắng viết ra cây cuộc gọi n = 5và hiện tại tôi đã đạt đến mức n == 3rất tốt, nhưng có lẽ đó chỉ là suy nghĩ bắt buộc của tôi khi nghĩ về điều này, nhưng điều đó không có nghĩa là n == 3, bạn chỉ cần hiểu map fib [0..]!!3? mà sau đó đi vào fib nchi nhánh của chương trình ... chính xác thì tôi nhận được lợi ích của dữ liệu được tính toán trước ở đâu?
Cà phê điện

1
Không, memoized_fibổn Điều slow_fibđó sẽ khiến bạn khóc nếu bạn theo dõi nó.
Kilian Foth
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.