Tối ưu hóa cuộc gọi đuôi có mặt trong nhiều ngôn ngữ và trình biên dịch. Trong tình huống này, trình biên dịch nhận ra một hàm có dạng:
int foo(n) {
...
return bar(n);
}
Ở đây, ngôn ngữ có thể nhận ra rằng kết quả được trả về là kết quả từ một hàm khác và thay đổi một lệnh gọi hàm với khung ngăn xếp mới thành một bước nhảy.
Nhận ra rằng phương pháp giai thừa cổ điển:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
không phải là cuộc gọi đuôi tối ưu vì kiểm tra cần thiết trên trở lại. ( Ví dụ mã nguồn và đầu ra được biên dịch )
Để thực hiện cuộc gọi đuôi này tối ưu hóa,
int _fact(int n, int acc) {
if(n == 1) return acc;
return _fact(n - 1, acc * n);
}
int factorial(int n) {
if(n == 0) return 1;
return _fact(n, 1);
}
Biên dịch mã này với gcc -O2 -S fact.c
(-O2 là cần thiết để cho phép tối ưu hóa trong trình biên dịch, nhưng với tối ưu hóa nhiều hơn -O3, con người khó đọc được ...)
_fact(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
( Ví dụ mã nguồn và đầu ra được biên dịch )
Người ta có thể thấy trong phân đoạn .L3
, jne
thay vì một call
(gọi một chương trình con với khung ngăn xếp mới).
Xin lưu ý rằng điều này đã được thực hiện với C. Tối ưu hóa cuộc gọi đuôi trong Java rất khó và phụ thuộc vào việc triển khai JVM (điều đó nói rằng, tôi chưa thấy bất kỳ điều gì làm điều đó, bởi vì nó khó và hàm ý của mô hình bảo mật Java được yêu cầu yêu cầu khung ngăn xếp - đó là những gì TCO tránh) - đệ quy đuôi + java và đệ quy đuôi + tối ưu hóa là các bộ thẻ tốt để duyệt. Bạn có thể thấy các ngôn ngữ JVM khác có thể tối ưu hóa đệ quy đuôi tốt hơn (thử clojure (yêu cầu recur để tối ưu hóa cuộc gọi đuôi) hoặc scala).
Mà nói,
Có một niềm vui nhất định khi biết rằng bạn đã viết một cái gì đó đúng - theo cách lý tưởng mà nó có thể được thực hiện.
Và bây giờ, tôi sẽ lấy một ít scotch và đưa vào một số điện tử Đức ...
Đối với câu hỏi chung về "các phương pháp để tránh tràn ngăn xếp trong thuật toán đệ quy" ...
Một cách tiếp cận khác là bao gồm một bộ đếm đệ quy. Đây là nhiều hơn để phát hiện các vòng lặp vô hạn gây ra bởi các tình huống ngoài tầm kiểm soát của một người (và mã hóa kém).
Bộ đếm đệ quy có dạng
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Mỗi lần bạn thực hiện một cuộc gọi, bạn sẽ tăng bộ đếm. Nếu bộ đếm quá lớn, bạn sẽ báo lỗi (ở đây, chỉ là trả về -1, mặc dù trong các ngôn ngữ khác, bạn có thể muốn ném ngoại lệ). Ý tưởng là để ngăn chặn những điều tồi tệ hơn xảy ra (lỗi bộ nhớ) khi thực hiện đệ quy sâu hơn nhiều so với dự kiến và có khả năng là một vòng lặp vô hạn.
Về lý thuyết, bạn không cần điều này. Trong thực tế, tôi đã thấy mã được viết kém đã gặp phải lỗi này do vô số lỗi nhỏ và thực tiễn mã hóa xấu (các vấn đề đồng thời đa luồng trong đó có gì đó thay đổi một cái gì đó bên ngoài phương thức khiến một luồng khác đi vào một vòng lặp vô hạn của các cuộc gọi đệ quy).
Sử dụng đúng thuật toán và giải quyết đúng vấn đề. Cụ thể cho Giả thuyết Collatz, có vẻ như bạn đang cố gắng giải quyết nó theo cách xkcd :
Bạn đang bắt đầu ở một số và thực hiện một giao dịch cây. Điều này nhanh chóng dẫn đến một không gian tìm kiếm rất lớn. Chạy nhanh để tính số lần lặp cho câu trả lời đúng cho kết quả trong khoảng 500 bước. Đây không phải là một vấn đề cho đệ quy với khung ngăn xếp nhỏ.
Mặc dù biết giải pháp đệ quy không phải là một điều xấu, người ta cũng nên nhận ra rằng nhiều lần giải pháp lặp lại là tốt hơn . Có thể thấy một số cách tiếp cận chuyển đổi thuật toán đệ quy sang thuật toán lặp trên Stack Overflow tại Way để đi từ đệ quy sang lặp .