Trình biên dịch có thể và chuyển đổi logic đệ quy thành logic không đệ quy tương đương không?


15

Tôi đã học F # và nó bắt đầu ảnh hưởng đến suy nghĩ của tôi khi tôi lập trình C #. Cuối cùng, tôi đã sử dụng đệ quy khi tôi cảm thấy kết quả cải thiện khả năng đọc và tôi không thể hình dung được nó đang cuộn ra thành một chồng tràn.

Điều này dẫn tôi đến việc liệu trình biên dịch có thể tự động chuyển đổi các hàm đệ quy thành một dạng không đệ quy tương đương hay không?


tối ưu hóa cuộc gọi đuôi là một ví dụ tốt nếu cơ bản nhưng nó chỉ hoạt động nếu bạn có return recursecall(args);đệ quy, công cụ phức tạp hơn có thể bằng cách tạo một ngăn xếp rõ ràng và cuộn nó xuống, nhưng tôi nghi ngờ chúng sẽ
ratchet freak

@ratchet freak: Recursion không có nghĩa là "tính toán đang sử dụng stack".
Giorgio

1
@Giorgio Tôi biết nhưng một ngăn xếp là cách dễ nhất để chuyển đổi đệ quy thành một vòng lặp
ratchet freak

Câu trả lời:


21

Có, một số ngôn ngữ và trình biên dịch sẽ chuyển đổi logic đệ quy thành logic không đệ quy. Điều này được gọi là tối ưu hóa cuộc gọi đuôi - lưu ý rằng không phải tất cả các cuộc gọi đệ quy đều được tối ưu hóa cuộc gọi đuôi. 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.

Để 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:
.LFB0:
        .cfi_startproc
        cmpl    $1, %edi
        movl    %esi, %eax
        je      .L2
        .p2align 4,,10
        .p2align 3
.L4:
        imull   %edi, %eax
        subl    $1, %edi
        cmpl    $1, %edi
        jne     .L4
.L2:
        rep
        ret
        .cfi_endproc

Người ta có thể thấy trong phân đoạn .L4, jnethay 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 - đệ quy đuôi + javađệ 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).


1
Tôi không chắc chắn rằng đây là những gì OP đang yêu cầu. Chỉ vì thời gian chạy không hoặc không tiêu tốn dung lượng ngăn xếp theo một cách nhất định, không có nghĩa là hàm không được đệ quy.

1
@MattFenwick Ý bạn thế nào? "Điều này dẫn tôi hỏi liệu trình biên dịch có thể tự động chuyển đổi các hàm đệ quy thành một dạng không đệ quy tương đương hay không" - câu trả lời là "có trong một số điều kiện nhất định". Các điều kiện được thể hiện và có một số gotcha trong một số ngôn ngữ phổ biến khác với tối ưu hóa cuộc gọi đuôi mà tôi đã đề cập.

9

Bước đi cẩn thận.

Câu trả lời là có, nhưng không phải lúc nào cũng vậy, và không phải tất cả chúng. Đây là một kỹ thuật có một vài tên khác nhau, nhưng bạn có thể tìm thấy một số thông tin khá dứt khoát ở đây và tại wikipedia .

Tôi thích cái tên "Tối ưu hóa cuộc gọi đuôi" nhưng có những người khác và một số người sẽ nhầm lẫn giữa thuật ngữ này.

Điều đó nói rằng có một vài điều quan trọng để nhận ra:

  • Để tối ưu hóa cuộc gọi đuôi, cuộc gọi đuôi yêu cầu các tham số được biết tại thời điểm cuộc gọi được thực hiện. Điều đó có nghĩa là nếu một trong các tham số là một lệnh gọi đến hàm, thì nó không thể được chuyển đổi thành một vòng lặp, bởi vì điều này sẽ yêu cầu lồng tùy ý của vòng lặp đã nói mà không thể mở rộng tại thời gian biên dịch.

  • C # không đáng tin cậy tối ưu hóa các cuộc gọi đuôi. IL có hướng dẫn để làm như vậy trình biên dịch F # sẽ phát ra, nhưng trình biên dịch C # sẽ phát ra nó không nhất quán và tùy thuộc vào tình huống JIT, JIT có thể hoặc không thể làm điều đó. Tất cả các dấu hiệu cho thấy bạn không nên dựa vào các cuộc gọi đuôi của mình được tối ưu hóa trong C #, nguy cơ tràn vào khi làm như vậy là rất quan trọng và có thật


1
Bạn có chắc chắn rằng đây là những gì OP đang yêu cầu? Như tôi đã đăng dưới câu trả lời khác, chỉ vì thời gian chạy không hoặc không tiêu tốn dung lượng ngăn xếp theo một cách nhất định, không có nghĩa là hàm không được đệ quy.

1
@MattFenwick thực sự là một điểm tuyệt vời, theo nghĩa thực tế, trình biên dịch F # phát ra các lệnh gọi đuôi hoàn toàn duy trì logic đệ quy, nó chỉ hướng dẫn JIT thực hiện nó theo kiểu thay thế không gian ngăn xếp thay vì không gian ngăn xếp- đang phát triển, tuy nhiên các trình biên dịch khác có thể biên dịch thành một vòng lặp. (Về mặt kỹ thuật, JIT đang biên dịch thành một vòng lặp hoặc thậm chí có thể là vòng lặp nếu vòng lặp hoàn toàn ở phía trước)
Jimmy Hoffa
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.