20

Gần đây tôi đã bắt đầu học Python và tôi khá ngạc nhiên khi thấy 1000 giới hạn đệ quy sâu (theo mặc định). Nếu bạn đặt nó đủ cao, khoảng 30000, nó bị treo với một lỗi phân đoạn giống như C. Mặc dù, C dường như đi khá cao hơn rất nhiều.Ngôn ngữ yêu thích của bạn xử lý đệ quy sâu như thế nào?

(The folks Python nhanh chóng chỉ ra rằng bạn luôn có thể chuyển đổi chức năng đệ quy cho những người lặp đi lặp lại và họ luôn nhanh hơn. Đó là 100% sự thật. Đó không phải là thực sự những gì câu hỏi của tôi là về mặc dù.)

Tôi đã thử thí nghiệm tương tự ở Perl và khoảng 10 triệu lượt truy cập nó tiêu thụ hết 4 hợp đồng ram của tôi và tôi đã sử dụng^C để ngừng thử. Rõ ràng Perl không sử dụng C stack, nhưng nó sử dụng một số lượng vô lý của bộ nhớ khi nó đệ quy - không khủng khiếp gây sốc xem xét bao nhiêu công việc nó đã làm để gọi chức năng.

Tôi đã thử ở Pike và hoàn toàn ngạc nhiên khi nhận được 100.000.000 lượt truy cập trong khoảng 2 giây. Tôi không có ý tưởng làm thế nào nó đã làm điều đó, nhưng tôi nghi ngờ nó làm phẳng đệ quy đến một quá trình lặp đi lặp lại - nó dường như không tiêu thụ bất kỳ bộ nhớ thêm trong khi nó hiện nó. [Lưu ý: Pike không flatten trường hợp tầm thường, nhưng segfaults về cái phức tạp hơn, hoặc vì vậy tôi nói với.]

tôi đã sử dụng các chức năng khác vô dụng:

int f(int i, int l) { if(i<l) return f(i+1,l); return i; } 

sub f { return f($_[0]+1, $_[1]) if $_[0]<$_[1]; return $_[0] }; 

def f(i,l): 
    if i<l: 
    return f(i+1,l) 
    return i 

Tôi rất tò mò ngôn ngữ như thế nào khác (ví dụ, PHP, Ruby, Java, Lua, Ocaml, Haskell) xử lý đệ quy và tại sao họ xử lý nó theo cách đó. Ngoài ra, xin lưu ý rằng nó có tạo sự khác biệt không nếu hàm này "đệ quy đuôi" (xem chú thích).

+3

Ví dụ của bạn là đệ quy đuôi, vì vậy bất kỳ triển khai ngôn ngữ nào hỗ trợ đệ quy đuôi sẽ chuyển đổi hiệu quả cuộc gọi đệ quy thành "goto". – mfx

Trả lời

20

"The Python folks được nhanh chóng chỉ ra rằng bạn luôn có thể chuyển đổi chức năng đệ quy cho những người lặp đi lặp lại và họ luôn nhanh hơn"

Điều này đúng, nhưng nếu nó thực sự dễ dàng như tất cả những gì, tại sao Python không làm điều đó cho tôi, để mã của tôi có thể trông đơn giản nhất có thể? (Tôi nói điều này không để slam Python thực hiện, nhưng vì câu trả lời giải thích vấn đề).

Tối ưu hóa đệ quy đã có mặt ở các ngôn ngữ chức năng kể từ, như thế kỷ 14 hoặc thứ gì đó. Việc triển khai Haskell, CAML, Lisp thường chuyển đổi ít nhất các hàm đệ quy thành các phép lặp: bạn có thể thực hiện điều này bằng cách nhận ra rằng có thể sắp xếp lại để không có biến cục bộ nào khác với giá trị trả về được sử dụng sau cuộc gọi đệ quy . Một mẹo để làm cho nó có thể nếu có một số công việc được thực hiện đối với giá trị trả về được đệ quy trước khi trả về, là giới thiệu một tham số "tích lũy" bổ sung. Nói một cách đơn giản, điều này có nghĩa là công việc có thể được thực hiện một cách hiệu quả trên đường "xuống" thay vì trên đường "lên": tìm kiếm xung quanh 'cách thực hiện một hàm đệ quy' để biết chi tiết. Các chi tiết thực tế của việc chuyển một hàm đệ qui đuôi thành vòng lặp về cơ bản là kích hoạt với quy ước cuộc gọi của bạn, như vậy bạn có thể "thực hiện cuộc gọi" đơn giản bằng cách thiết lập các tham số và nhảy trở lại phần đầu của hàm, mà không làm phiền để lưu tất cả nội dung đó vào phạm vi mà bạn biết bạn sẽ không bao giờ sử dụng. Trong các thuật ngữ lắp ráp, bạn không cần phải lưu giữ sổ đăng ký lưu trữ người gọi nếu phân tích luồng dữ liệu cho bạn biết chúng không được sử dụng ngoài cuộc gọi, và điều tương tự cũng xảy ra đối với mọi thứ trong ngăn xếp: bạn không phải di chuyển con trỏ ngăn xếp trên một cuộc gọi nếu bạn không nhớ "của bạn" bit stack được scribbled hơn bởi đệ quy tiếp theo/lặp đi lặp lại.

Trái với cách bạn diễn giải các folks Python, chuyển đổi một hàm đệ quy chung để một lần lặp là không nhỏ: ví dụ nếu đó là nhân-đệ quy sau đó trong một phương pháp đơn giản bạn vẫn sẽ cần một chồng.

Ghi nhớ là một kỹ thuật hữu ích, tuy nhiên, đối với các hàm đệ quy tùy ý, bạn có thể muốn tìm kiếm nếu bạn quan tâm đến các phương pháp có thể có. Điều đó có nghĩa là mỗi khi một hàm được đánh giá, bạn sẽ kết quả trong bộ nhớ cache. Để sử dụng điều này để tối ưu đệ quy, về cơ bản, nếu hàm đệ quy của bạn đếm "xuống", và bạn ghi nhớ nó, thì bạn có thể đánh giá nó lặp lại bằng cách thêm một vòng lặp đếm "lên" tính toán từng giá trị của hàm lần lượt cho đến khi bạn đạt đến Mục tiêu. Điều này sử dụng rất ít không gian ngăn xếp với điều kiện là bộ nhớ đệm ghi nhớ đủ lớn để giữ tất cả các giá trị bạn cần: ví dụ nếu f (n) phụ thuộc vào f (n-1), f (n-2) và f (n -3) bạn chỉ cần không gian cho 3 giá trị trong bộ đệm: khi bạn đi lên, bạn có thể đá cái thang đi. Nếu f (n) phụ thuộc vào f (n-1) và f (n/2), bạn cần nhiều không gian trong bộ nhớ cache, nhưng vẫn ít hơn bạn sử dụng cho ngăn xếp trong một đệ quy chưa được tối ưu hóa.

+2

+1 cho "lý do tại sao Python không làm điều đó cho tôi" ... nhiều người sẽ không đánh giá cao sự mỉa mai ngọt ngào có liên quan ở đây. – Ingo

4

PHP có một giới hạn mặc định là 100 trước khi nó chết:

Fatal error: Maximum function nesting level of '100' reached, aborting!

Chỉnh sửa: Bạn có thể thay đổi giới hạn với ini_set('xdebug.max_nesting_level', 100000);, nhưng nếu bạn đi trên khoảng 1150 lặp PHP tai nạn:

[Fri Oct 24 11:39:41 2008] [notice] Parent: child process exited with status 3221225477 -- Restarting.

+0

Đây không phải là cách PHP xử lý "đệ quy sâu", nhưng cách mà phần mở rộng PHP xdebug xử lý nó. PHP không xử lý đệ quy nào cả. Nó chỉ treo/segfaults. –

2

Theo chủ đề này, around 5,000,000 với java, RAM 1Gb. (và rằng, với phiên bản 'khách hàng' của điểm phát sóng)

Đó là với stack (-Xss) trong 300Mo.

Với số -server option, có thể tăng lên.

Ngoài ra, người ta cũng có thể thử tối ưu hóa trình biên dịch (ví dụ: with JET) để giảm chi phí của ngăn xếp ở mỗi lớp.

3

C# /. NET sẽ sử dụng đệ quy đuôi trong một nhóm trường hợp cụ thể. (Trình biên dịch C# không phát ra một mã opcode tailcall, nhưng là JIT will implement tail recursion in some cases.

Shri Borde also has a post on this topic.Tất nhiên, CLR liên tục thay đổi, và với .NET 3.5 và 3.5SP1 nó có thể đã thay đổi một lần nữa liên quan đến đuôi cuộc gọi

1

Hình ảnh Dataflex sẽ ngăn xếp tràn.

2

Trong một số trường hợp không bệnh lý (như của bạn), (mới nhất) Lua sẽ sử dụng tail call recursion, tức là. nó sẽ chỉ nhảy mà không đẩy dữ liệu vào ngăn xếp. Vì vậy, số vòng lặp đệ quy có thể gần như không giới hạn.

Thử nghiệm với:

function f(i, l) 
    if i < l then 
     return f(i+1, l) 
    end 
    return i 
end 

local val1 = arg[1] or 1 
local val2 = arg[2] or 100000000 
print(f(val1 + 0, val2 + 0)) 

Ngoài ra với:

function g(i, l) 
    if i >= l then 
     return i 
    end 
    return g(i+1, l) 
end 

và thậm chí cố gắng cross-đệ quy (f gọi g và g gọi f ...).

Trên Windows, Lua 5.1 sử dụng khoảng 1,1MB (không đổi) để chạy điều này, kết thúc sau vài giây.

3

Sử dụng sau trong F # console tương tác, nó chạy trong vòng chưa đầy một giây:

let rec f i l = 
    match i with 
    | i when i < l -> f (i+1) l 
    | _ -> l 

f 0 100000000;; 

sau đó tôi đã cố gắng một bản dịch thẳng ví dụ:

let rec g i l = if i < l then g (i+1) l else l 

g 0 100000000;; 

Cùng kết quả nhưng biên dịch khác nhau.

Đây là những gì f trông như thế nào trong khi dịch sang C#:

int f(int i, int l) 
{ 
    while(true) 
    { 
    int num = i; 
    if(num >= l) 
     return l; 
    int i = num; 
    l = l; 
    i = i + 1; 
    } 
} 

g, tuy nhiên được phiên dịch sang này:

int g(int i, int l) 
{ 
    while(i < l) 
    { 
    l = l; 
    i++; 
    } 
    return l; 
} 

Điều thú vị là hai chức năng mà về cơ bản giống nhau được kết xuất khác nhau bởi trình biên dịch F #. Nó cũng cho thấy trình biên dịch F # có tối ưu đệ quy đuôi. Vì vậy, điều này nên lặp cho đến khi tôi đạt đến giới hạn cho số nguyên 32-bit.

6

Đây là câu hỏi thực hiện nhiều hơn câu hỏi ngôn ngữ. Không có gì ngăn chặn một số (stoopid) C trình biên dịch triển khai từ cũng hạn chế stack cuộc gọi của họ đến 1000. Có rất nhiều bộ vi xử lý nhỏ ra khỏi đó mà sẽ không có không gian ngăn xếp cho dù nhiều.

(The folks Python nhanh chóng chỉ ra rằng bạn luôn có thể chuyển đổi chức năng đệ quy cho những người lặp đi lặp lại và họ luôn nhanh hơn. Đó là 100% sự thật. Đó không phải là thực sự những gì câu hỏi của tôi là về mặc dù.)

Có thể họ nói như vậy, nhưng điều này không hoàn toàn chính xác. Việc đệ quy luôn có thể được chuyển đổi thành lặp lại, nhưng đôi khi nó cũng yêu cầu sử dụng thủ công một ngăn xếp ngăn xếp. Trong những trường hợp đó, tôi có thể thấy phiên bản đệ quy nhanh hơn (giả sử bạn đủ thông minh để thực hiện tối ưu hóa đơn giản, như kéo các khai báo không cần thiết bên ngoài thường trình đệ qui). Sau khi tất cả, ngăn xếp đẩy xung quanh các cuộc gọi thủ tục là một vấn đề cũng bị ràng buộc rằng trình biên dịch của bạn nên biết làm thế nào để tối ưu hóa rất tốt. Các hoạt động ngăn xếp thủ công, mặt khác, sẽ không có mã tối ưu hóa chuyên biệt trong trình biên dịch của bạn, và chịu trách nhiệm có tất cả các loại kiểm tra sanity giao diện người dùng sẽ chiếm các chu kỳ bổ sung.

Có thể là trường hợp giải pháp lặp/ngăn xếp luôn nhanh hơn bằng Python. Nếu vậy, đó là một thất bại của Python, không phải đệ quy.

1

Có cách để cải thiện mã Perl, để làm cho nó sử dụng ngăn xếp kích thước không đổi. Bạn làm điều này bằng cách sử dụng một hình thức đặc biệt của goto.

sub f{ 
    if($_[0] < $_[1]){ 

    # return f($_[0]+1, $_[1]); 

    @_ = ($_[0]+1, $_[1]); 
    goto &f; 

    } else { 
    return $_[0] 
    } 
} 

Khi được gọi đầu tiên sẽ phân bổ không gian trên ngăn xếp. Sau đó, nó sẽ thay đổi các đối số của nó và khởi động lại chương trình con, mà không cần thêm bất kỳ thứ gì khác vào ngăn xếp. Do đó, nó sẽ giả vờ rằng nó không bao giờ được gọi là bản thân nó, thay đổi nó thành một quá trình lặp đi lặp lại.


Bạn cũng có thể sử dụng mô-đun Sub::Call::Recur. Điều này làm cho mã dễ hiểu hơn và ngắn hơn.

use Sub::Call::Recur; 
sub f{ 
    recur($_[0]+1, $_[1]) if $_[0] < $_[1]; 
    return $_[0]; 
} 
1

Tôi khá một fan hâm mộ của lập trình chức năng, và vì hầu hết những langauges thực hiện tối ưu hóa cuộc gọi đuôi, bạn có thể recurse nhiều như bạn thích :-P

Tuy nhiên, trên thực tế, tôi phải sử dụng rất nhiều Java và sử dụng Python rất nhiều. Không biết Java giới hạn là bao nhiêu, nhưng đối với Python tôi đã thực sự lên kế hoạch (nhưng vẫn chưa thực hiện nó) để thực hiện một trình trang trí có thể gọi điện tối ưu hóa chức năng được trang trí. Tôi đã lên kế hoạch để không tối ưu hóa đệ quy, nhưng chủ yếu là tập thể dục trong việc tự động vá mã byte của Python và tìm hiểu thêm về nội bộ Pythons. Heres một số liên kết itneresting: http://lambda-the-ultimate.org/node/1331http://www.rowehl.com/blog/?p=626

2

Chạy ruby ​​1.9.2dev (2010/07/11 sửa đổi 28.618) [x86_64-darwin10.0.0] trên macbook trắng cũ:

def f 
    @i += 1 
    f 
end 

@i = 0 

begin 
    f 
rescue SystemStackError 
    puts @i 
end 

đầu ra 9353 cho tôi, có nghĩa là Ruby khao khát với ít hơn 10.000 cuộc gọi trên ngăn xếp.

Với cross-đệ quy, chẳng hạn như:

def f 
    @i += 1 
    g 
end 

def g 
    f 
end 

nó craps ra trong một nửa thời gian, tại 4677 (~ = 9353/2).

tôi có thể bóp ra một vài lần lặp hơn bằng cách gói các cuộc gọi đệ quy trong một proc:

def f 
    @i += 1 
    yield 
end 

@i = 0 
@block = lambda { f(&@block) } 

begin 
    f(&@block) 
rescue SystemStackError 
    puts @i 
end 

mà được lên đến 4850 trước khi erroring ra.

1

clojure cung cấp một biểu mẫu đặc biệt để đệ quy đuôi "lặp lại" điều này chỉ có thể được sử dụng ở các vị trí đuôi của ast. Nếu không nó hoạt động như java và có khả năng sẽ ném một StackverflowException.

Các vấn đề liên quan