Đệ quy đuôi có nhanh hơn trong Python không?

Trong trang này, chúng ta sẽ xem xét đệ quy lệnh gọi đuôi và xem cách buộc Python cho phép chúng ta loại bỏ các lệnh gọi đuôi bằng cách sử dụng tấm bạt lò xo. Chúng tôi sẽ trải qua hai lần lặp lại thiết kế. đầu tiên là làm cho nó hoạt động và thứ hai là cố gắng làm cho cú pháp có vẻ hợp lý. Bản thân tôi sẽ không coi đây là một kỹ thuật hữu ích, nhưng tôi nghĩ đó là một ví dụ điển hình thể hiện một số sức mạnh của người trang trí

Điều đầu tiên chúng ta nên rõ ràng là định nghĩa của một cuộc gọi đuôi. Phần "gọi" có nghĩa là chúng ta đang xem xét các lời gọi hàm và phần "đuôi" có nghĩa là, trong số đó, chúng ta đang xem xét các lời gọi là điều cuối cùng mà một hàm thực hiện trước khi nó trả về. Trong ví dụ sau, lệnh gọi đệ quy tới f là lệnh gọi đuôi [việc sử dụng biến ret là không quan trọng vì nó chỉ kết nối kết quả của lệnh gọi tới f với câu lệnh trả về] và lệnh gọi tới g không phải là lệnh gọi đuôi

def f[n] :
    if n > 0 :
        n -= 1
        ret = f[n]
        return ret
    else :
        ret = g[n]
        return ret + 1

1. Tại sao cuộc gọi đuôi lại quan trọng

Các cuộc gọi đuôi đệ quy có thể được thay thế bằng các bước nhảy. Đây được gọi là "loại bỏ cuộc gọi đuôi" và là một phép biến đổi có thể giúp giới hạn độ sâu ngăn xếp tối đa được sử dụng bởi hàm đệ quy, với lợi ích là giảm lưu lượng bộ nhớ bằng cách không phải phân bổ các khung ngăn xếp. Đôi khi, hàm đệ quy thường không thể chạy do tràn ngăn xếp được chuyển thành hàm có thể

Do các lợi ích, một số trình biên dịch [như gcc] thực hiện loại bỏ lệnh gọi đuôi[1], thay thế các lệnh gọi đuôi đệ quy bằng các bước nhảy [và, tùy thuộc vào ngôn ngữ và hoàn cảnh, đôi khi các lệnh gọi đuôi đến các chức năng khác có thể được thay thế bằng việc xoa bóp ngăn xếp và một bước nhảy . Trong ví dụ sau, chúng tôi sẽ loại bỏ các lệnh gọi đuôi trong một đoạn mã thực hiện tìm kiếm nhị phân. Nó có hai cuộc gọi đuôi đệ quy

def binary_search[x, lst, low=None, high=None] :
    if low == None : low = 0
    if high == None : high = len[lst]-1
    mid = low + [high - low] // 2
    if low > high :
        return None
    elif lst[mid] == x :
        return mid
    elif lst[mid] > x :
        return binary_search[x, lst, low, mid-1]
    else :
        return binary_search[x, lst, mid+1, high]
Giả sử Python có câu lệnh goto, chúng ta có thể thay thế các lệnh gọi đuôi bằng một bước nhảy tới đầu hàm, sửa đổi các đối số tại các vị trí lệnh gọi một cách thích hợp.
def binary_search[x, lst, low=None, high=None] :
  start:
    if low == None : low = 0
    if high == None : high = len[lst]-1
    mid = low + [high - low] // 2
    if low > high :
        return None
    elif lst[mid] == x :
        return mid
    elif lst[mid] > x :
        [x, lst, low, high] = [x, lst, low, mid-1]
        goto start
    else :
        [x, lst, low, high] = [x, lst, mid+1, high]
        goto start
mà người ta có thể quan sát thấy, có thể được viết bằng Python thực tế như
def binary_search[x, lst, low=None, high=None] :
    if low == None : low = 0
    if high == None : high = len[lst]-1
    while True :
        mid = low + [high - low] // 2
        if low > high :
            return None
        elif lst[mid] == x :
            return mid
        elif lst[mid] > x :
            high = mid - 1
        else :
            low = mid + 1
Tôi chưa kiểm tra sự khác biệt về tốc độ giữa phiên bản lặp này và phiên bản đệ quy ban đầu, nhưng tôi hy vọng nó sẽ nhanh hơn một chút vì có ít bộ nhớ hơn nhiều

Thật không may, việc chuyển đổi khiến việc chứng minh tìm kiếm nhị phân là chính xác trong mã kết quả trở nên khó khăn hơn. Với thuật toán đệ quy ban đầu, nó gần như tầm thường bằng quy nạp

Các ngôn ngữ lập trình như Scheme phụ thuộc vào việc loại bỏ các cuộc gọi đuôi cho luồng điều khiển và nó cũng cần thiết cho kiểu truyền tiếp tục. [2]

2. Một nỗ lực đầu tiên

Ví dụ đang chạy của chúng ta sẽ là hàm giai thừa [cổ điển], được viết với một đối số bộ tích lũy để lời gọi đệ quy của nó là một lời gọi đuôi

def fact[n, r=1] :
    if n  high :
        return None
    elif lst[mid] == x :
        return mid
    elif lst[mid] > x :
        return binary_search[x, lst, low, mid-1]
    else :
        return binary_search[x, lst, mid+1, high]
1

Và sau đó chúng ta có thể gọi thực tế trực tiếp với số lượng lớn

Ngoài ra, không giống như trong lần thử đầu tiên, giờ đây chúng ta có thể có các hàm đệ quy lẫn nhau, tất cả đều thực hiện các lệnh gọi đuôi. Đối tượng TailCall được gọi đầu tiên sẽ xử lý tất cả các thao tác tramolining

Nếu muốn, chúng ta cũng có thể định nghĩa hàm sau để làm cho danh sách đối số cho lệnh gọi đuôi phù hợp hơn với danh sách đối số cho lệnh gọi hàm thông thường. [3]

def binary_search[x, lst, low=None, high=None] :
    if low == None : low = 0
    if high == None : high = len[lst]-1
    mid = low + [high - low] // 2
    if low > high :
        return None
    elif lst[mid] == x :
        return mid
    elif lst[mid] > x :
        return binary_search[x, lst, low, mid-1]
    else :
        return binary_search[x, lst, mid+1, high]
2và sau đó thực tế có thể được viết lại thành
def binary_search[x, lst, low=None, high=None] :
    if low == None : low = 0
    if high == None : high = len[lst]-1
    mid = low + [high - low] // 2
    if low > high :
        return None
    elif lst[mid] == x :
        return mid
    elif lst[mid] > x :
        return binary_search[x, lst, low, mid-1]
    else :
        return binary_search[x, lst, mid+1, high]
3

Người ta hy vọng rằng việc đánh dấu các cuộc gọi đuôi theo cách thủ công có thể được thực hiện, nhưng tôi không thể nghĩ ra cách nào để phát hiện xem một cuộc gọi có phải là cuộc gọi đuôi hay không mà không cần kiểm tra mã nguồn. Có lẽ một ý tưởng cho công việc tiếp theo là thuyết phục Guido von Rossum rằng Python nên hỗ trợ đệ quy đuôi [điều này rất khó xảy ra]

Là đệ quy đuôi nhanh hơn?

Theo nguyên tắc thông thường; . Đó là bởi vì điều đó yêu cầu một lần lặp lại trên toàn bộ danh sách. Hàm đệ quy đuôi thường nhanh hơn trong việc rút gọn danh sách, như ví dụ đầu tiên của chúng tôi. tail-recursive functions are faster if they don't need to reverse the result before returning it. That's because that requires another iteration over the whole list. Tail-recursive functions are usually faster at reducing lists, like our first example.

Đệ quy đuôi có nhanh hơn không

Nói một cách đơn giản, trong đệ quy đuôi, hàm đệ quy được gọi lần cuối. Vì vậy, nó hiệu quả hơn so với đệ quy không đuôi . Ngoài ra, trình biên dịch có thể dễ dàng tối ưu hóa hàm đệ quy đuôi, vì không còn bất kỳ lệnh nào được thực thi vì lời gọi đệ quy là câu lệnh cuối cùng.

Đệ quy đuôi có tốt hơn không?

Các lệnh gọi đệ quy đuôi cũng tốt hơn hầu hết các lệnh gọi đuôi [mặc dù không phải tất cả các lệnh gọi đuôi khác] vì bạn đang gọi cùng một chức năng. Vì vậy, bạn sẽ luôn chuyển cùng một số đối số theo cùng một định dạng

Tại sao Python không hỗ trợ đệ quy đuôi?

Lý do của giới hạn này là [trong số những lý do khác] việc thực hiện các cuộc gọi đệ quy cần rất nhiều bộ nhớ và tài nguyên vì mỗi khung trong ngăn xếp cuộc gọi phải được duy trì cho đến khi cuộc gọi hoàn tất . .

Chủ Đề