Tại sao các đánh giá tối ưu calcul-tính toán có thể tính toán lũy thừa mô-đun lớn mà không có công thức?


135

Số nhà thờ là một mã hóa của số tự nhiên như chức năng.

(\ f x  (f x))             -- church number 1
(\ f x  (f (f (f x))))     -- church number 3
(\ f x  (f (f (f (f x))))) -- church number 4

Nhanh chóng, bạn có thể lũy thừa 2 số nhà thờ chỉ bằng cách áp dụng chúng. Đó là, nếu bạn áp dụng 4 đến 2, bạn sẽ có được số nhà thờ 16, hoặc 2^4. Rõ ràng, điều đó là hoàn toàn không thực tế. Số nhà thờ cần một lượng bộ nhớ tuyến tính và thực sự, rất chậm. Việc tính toán một cái gì đó như 10^10- mà GHCI nhanh chóng trả lời chính xác - sẽ mất nhiều thời gian và không thể phù hợp với bộ nhớ trên máy tính của bạn.

Gần đây tôi đã thử nghiệm với người đánh giá tối ưu. Trong các bài kiểm tra của mình, tôi đã vô tình gõ những thứ sau vào-comp tối ưu của mình:

10 ^ 10 % 13

Nó được cho là nhân, không phải lũy thừa. Trước khi tôi có thể di chuyển các ngón tay của mình để hủy bỏ chương trình chạy mãi mãi trong tuyệt vọng, nó đã trả lời yêu cầu của tôi:

3
{ iterations: 11523, applications: 5748, used_memory: 27729 }

real    0m0.104s
user    0m0.086s
sys     0m0.019s

Với đèn flash "thông báo lỗi", tôi đã truy cập Google và xác minh 10^10%13 == 3. Nhưng calculator máy tính không cần phải tìm thấy kết quả đó, nó chỉ có thể lưu trữ 10 ^ 10. Tôi bắt đầu nhấn mạnh nó, cho khoa học. Nó ngay lập tức trả lời tôi 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Tôi đã phải sử dụng các công cụ bên ngoài để xác minh các kết quả đó, vì bản thân Haskell không thể tính toán được (do tràn số nguyên) (tất nhiên là nếu bạn sử dụng Số nguyên không phải là Ints!). Đẩy nó đến giới hạn của nó, đây là câu trả lời cho 200^200%31:

5
{ iterations: 10351327, applications: 5175644, used_memory: 23754870 }

real    0m4.025s
user    0m3.686s
sys 0m0.341s

Nếu chúng ta có một bản sao vũ trụ cho mỗi nguyên tử trên vũ trụ và chúng ta có một máy tính cho mỗi nguyên tử mà chúng ta có, chúng ta không thể lưu trữ số nhà thờ 200^200. Điều này khiến tôi đặt câu hỏi nếu máy mac của tôi thực sự mạnh đến thế. Có lẽ người đánh giá tối ưu đã có thể bỏ qua các nhánh không cần thiết và đi đến câu trả lời theo cách tương tự mà Haskell làm với đánh giá lười biếng. Để kiểm tra điều này, tôi đã biên dịch chương trình to thành Haskell:

data Term = F !(Term -> Term) | N !Double
instance Show Term where {
    show (N x) = "(N "++(if fromIntegral (floor x) == x then show (floor x) else show x)++")";
    show (F _) = "(λ...)"}
infixl 0 #
(F f) # x = f x
churchNum = F(\(N n)->F(\f->F(\x->if n<=0 then x else (f#(churchNum#(N(n-1))#f#x)))))
expMod    = (F(\v0->(F(\v1->(F(\v2->((((((churchNum # v2) # (F(\v3->(F(\v4->(v3 # (F(\v5->((v4 # (F(\v6->(F(\v7->(v6 # ((v5 # v6) # v7))))))) # v5))))))))) # (F(\v3->(v3 # (F(\v4->(F(\v5->v5)))))))) # (F(\v3->((((churchNum # v1) # (churchNum # v0)) # ((((churchNum # v2) # (F(\v4->(F(\v5->(F(\v6->(v4 # (F(\v7->((v5 # v7) # v6))))))))))) # (F(\v4->v4))) # (F(\v4->(F(\v5->(v5 # v4))))))) # ((((churchNum # v2) # (F(\v4->(F(\v5->v4))))) # (F(\v4->v4))) # (F(\v4->v4))))))) # (F(\v3->(((F(\(N x)->F(\(N y)->N(x+y)))) # v3) # (N 1))))) # (N 0))))))))
main = print $ (expMod # N 5 # N 5 # N 4)

Điều này chính xác đầu ra 1( 5 ^ 5 % 4) - nhưng ném bất cứ điều gì ở trên 10^10và nó sẽ bị mắc kẹt, loại bỏ giả thuyết.

Trình đánh giá tối ưu mà tôi đã sử dụng là một chương trình JavaScript dài, không tối ưu hóa dài 160 dòng, không bao gồm bất kỳ loại toán mô đun hàm mũ nào - và hàm mô đun lambda-tính toán tôi sử dụng cũng đơn giản không kém:

ab.(bcd.(ce.(dfg.(f(efg)))e))))(λc.(cde.e)))(λc.(a(bdef.(dg.(egf))))(λd.d)(λde.(ed)))(bde.d)(λd.d)(λd.d))))))

Tôi không sử dụng thuật toán hoặc công thức số học mô-đun cụ thể. Vì vậy, làm thế nào là người đánh giá tối ưu có thể đi đến câu trả lời đúng?


2
Bạn có thể cho chúng tôi biết thêm về loại đánh giá tối ưu bạn sử dụng? Có lẽ là một trích dẫn giấy? Cảm ơn!
Jason Dagit

11
Tôi đang sử dụng thuật toán trừu tượng của Lamping, như được giải thích trong cuốn sách Ngôn ngữ lập trình chức năng tối ưu . Lưu ý rằng tôi không sử dụng "oracle" (không có croissant / ngoặc) vì thuật ngữ đó có thể đánh máy bằng EAL. Ngoài ra, thay vì giảm một cách ngẫu nhiên người hâm mộ tại song song, tôi liên tục đi qua đồ thị như để không làm giảm các nút unreachable, nhưng tôi sợ điều này không phải là về văn học AFAIK ...
MaiaVictor

7
Được rồi, trong trường hợp có ai tò mò, tôi đã thiết lập kho lưu trữ GitHub với mã nguồn cho người đánh giá tối ưu của tôi. Nó có nhiều ý kiến ​​và bạn có thể kiểm tra nó đang chạy node test.js. Hãy cho tôi biết nếu bạn có bất kỳ câu hỏi.
MaiaVictor 29/07/2015

1
Tìm gọn gàng! Tôi không biết đủ về đánh giá tối ưu, nhưng tôi có thể nói rằng điều này khiến tôi nhớ đến Định lý nhỏ của Fermat / Định lý Euler. Nếu bạn không biết về nó, nó có thể là một điểm khởi đầu tốt.
luqui

5
Đây là lần đầu tiên tôi không có manh mối nhỏ nhất về câu hỏi đó là gì, nhưng vẫn nêu lên câu hỏi, và đặc biệt, câu trả lời đầu tiên xuất sắc.
Marco13

Câu trả lời:


124

Hiện tượng này xuất phát từ số lượng các bước giảm beta được chia sẻ, có thể khác biệt đáng kể trong đánh giá lười biếng kiểu Haskell (hoặc gọi theo giá trị thông thường, không quá xa về mặt này) và trong Vuillemin-Lévy-Lamping- Đánh giá "tối ưu" của Kathail-Asperti-Guerrini- (et al.). Đây là một tính năng chung, hoàn toàn độc lập với các công thức số học mà bạn có thể sử dụng trong ví dụ cụ thể này.

Chia sẻ có nghĩa là có một đại diện cho thuật ngữ lambda của bạn trong đó một "nút" có thể mô tả một số phần tương tự của thuật ngữ lambda thực tế mà bạn đại diện. Chẳng hạn, bạn có thể đại diện cho thuật ngữ

\x. x ((\y.y)a) ((\y.y)a)

sử dụng một biểu đồ (theo chu kỳ có hướng) trong đó chỉ có một lần xuất hiện của biểu đồ con (\y.y)avà hai cạnh nhắm vào biểu đồ con đó. Theo thuật ngữ của Haskell, bạn có một thunk, mà bạn chỉ đánh giá một lần và hai con trỏ cho thunk này.

Ghi nhớ theo kiểu Haskell thực hiện chia sẻ các tập hợp con hoàn chỉnh. Mức chia sẻ này có thể được biểu diễn bằng các biểu đồ chu kỳ có hướng. Chia sẻ tối ưu không có hạn chế này: nó cũng có thể chia sẻ các tập con "một phần", có thể ngụ ý các chu kỳ trong biểu diễn đồ thị.

Để thấy sự khác biệt giữa hai cấp độ chia sẻ này, hãy xem xét thuật ngữ

\x. (\z.z) ((\z.z) x)

Nếu việc chia sẻ của bạn bị hạn chế để hoàn thành các tập hợp con như trường hợp của Haskell, bạn có thể chỉ có một lần xuất hiện \z.z, nhưng hai phiên bản beta ở đây sẽ khác biệt: một là (\z.z) xvà một là khác (\z.z) ((\z.z) x), và vì chúng không phải là các điều khoản bằng nhau chúng không thể được chia sẻ. Nếu việc chia sẻ các bộ con một phần được cho phép, thì có thể chia sẻ thuật ngữ một phần (\z.z) [](đó không chỉ là hàm \z.z, mà là "hàm \z.zđược áp dụng cho một cái gì đó ), đánh giá trong một bước để chỉ một thứ gì đó , bất kể đối số này là gì. bạn có thể có một biểu đồ trong đó chỉ có một nút đại diện cho hai ứng dụng của\z.zthành hai đối số riêng biệt và trong đó hai ứng dụng này có thể được giảm chỉ trong một bước. Lưu ý rằng có một chu kỳ trên nút này, vì đối số của "lần xuất hiện đầu tiên" chính xác là "lần xuất hiện thứ hai". Cuối cùng, với việc chia sẻ tối ưu, bạn có thể đi từ (biểu đồ biểu thị) \x. (\z.z) ((\z.z) x))đến (biểu đồ biểu thị) kết quả \x.xchỉ trong một bước giảm beta (cộng với một số sổ sách kế toán). Về cơ bản, điều này xảy ra trong trình đánh giá tối ưu của bạn (và biểu diễn đồ thị cũng là thứ ngăn chặn sự bùng nổ không gian).

Đối với các giải thích mở rộng một chút, bạn có thể xem phần Tối ưu yếu của bài báo và Ý nghĩa của việc chia sẻ (điều bạn quan tâm là phần giới thiệu và phần 4.1, và có thể một số gợi ý thư mục ở cuối).

Trở lại ví dụ của bạn, mã hóa các hàm số học làm việc trên các số nguyên Church là một trong những ví dụ "nổi tiếng" trong đó các nhà đánh giá tối ưu có thể thực hiện tốt hơn các ngôn ngữ chính (trong câu này, nổi tiếng thực sự có nghĩa là một số ít các chuyên gia nhận thức được các ví dụ này). Để biết thêm các ví dụ như vậy, hãy xem bài báo Người vận hành an toàn: Chân đế đã đóng vĩnh viễn bởi Asperti và Chroboczek (và nhân tiện, bạn sẽ tìm thấy ở đây các thuật ngữ lambda thú vị không thể đánh máy bằng EAL; vì vậy tôi khuyến khích bạn nên dùng một cái nhìn vào các nhà tiên tri, bắt đầu với bài báo Asperti / Chroboczek này).

Như bạn đã nói, loại mã hóa này hoàn toàn không thực tế, nhưng chúng vẫn thể hiện một cách tốt đẹp để hiểu những gì đang diễn ra. Và hãy để tôi kết luận với một thách thức để điều tra thêm: bạn có thể tìm thấy một ví dụ về việc đánh giá tối ưu trên các bảng mã được cho là xấu này thực sự ngang bằng với đánh giá truyền thống về biểu diễn dữ liệu hợp lý không? (theo như tôi biết đây là một câu hỏi mở thực sự).


34
Đó là một bài viết đầu tiên kỹ lưỡng nhất. Chào mừng bạn đến với StackOverflow!
dfeuer

2
Không có gì ít hơn sâu sắc. Cảm ơn bạn, và chào mừng đến với cộng đồng!
MaiaVictor

7

Đây không phải là một anwser nhưng nó là một gợi ý về nơi bạn có thể bắt đầu tìm kiếm.

Có một cách tầm thường để tính toán lũy thừa mô đun trong không gian nhỏ, đặc biệt bằng cách viết lại

(a * x ^ y) % z

như

(((a * x) % z) * x ^ (y - 1)) % z

Nếu một người đánh giá đánh giá như thế này và giữ tham số tích lũy aở dạng bình thường thì bạn sẽ tránh sử dụng quá nhiều không gian. Nếu thực sự người đánh giá của bạn tối ưu thì có lẽ nó không được thực hiện bất kỳ công việc nào nhiều hơn công việc này, vì vậy, đặc biệt không thể sử dụng nhiều không gian hơn thời gian mà người này phải đánh giá.

Tôi không thực sự chắc chắn những gì một người đánh giá tối ưu thực sự là vì vậy tôi sợ tôi không thể làm cho điều này trở nên khắt khe hơn.


4
@Viclib Fibonacci như @Tom nói là một ví dụ tốt. fibđòi hỏi thời gian theo cấp số nhân theo cách ngây thơ, có thể giảm xuống tuyến tính với một chương trình ghi nhớ / lập trình động đơn giản. Ngay cả thời gian logarit (!) Cũng có thể thông qua việc tính toán sức mạnh ma trận thứ n của [[0,1],[1,1]](miễn là bạn đếm từng phép nhân để có chi phí không đổi).
chi

1
Thậm chí thời gian không đổi nếu bạn đủ táo bạo để ước chừng :)
J. Abrahamson

5
@TomEllis Tại sao một cái gì đó chỉ biết làm thế nào để giảm các biểu thức tính toán lambda tùy ý có ý tưởng nào (a * b) % n = ((a % n) * b) % nmặc dù? Đó là phần bí ẩn chắc chắn.
Reid Barton

2
@ReidBarton chắc chắn tôi đã thử rồi! Kết quả tương tự, mặc dù.
MaiaVictor

2
@TomEllis và Chi, mặc dù chỉ có một nhận xét nhỏ. Tất cả đều cho rằng chức năng đệ quy truyền thống là triển khai sợi "ngây thơ", nhưng IMO có một cách khác để diễn đạt nó tự nhiên hơn nhiều. Dạng bình thường của biểu diễn mới đó có một nửa kích thước của biểu tượng truyền thống) và Optlam quản lý để tính toán một cách tuyến tính! Vì vậy, tôi cho rằng đó là định nghĩa "ngây thơ" của sợi liên quan đến tính toán. Tôi sẽ tạo một bài đăng trên blog nhưng tôi không chắc nó thực sự đáng giá ...
MaiaVictor
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.