Bạn đang thử nghiệm chính xác: chức năng của bạn thực sự không phải là đệ quy đuôi. Để tìm hiểu, bạn có thể biên dịch mã của mình bằng cách sử dụng erlc -S <erlang source file>
.
{function, sum_list, 2, 2}.
{label,1}.
{func_info,{atom,so},{atom,sum_list},2}.
{label,2}.
{test,is_nonempty_list,{f,3},[{x,0}]}.
{allocate,1,2}.
{get_list,{x,0},{y,0},{x,0}}.
{call,2,{f,2}}.
{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}.
{deallocate,1}.
return.
{label,3}.
{test,is_nil,{f,1},[{x,0}]}.
{move,{x,1},{x,0}}.
return.
Như một sự so sánh các phiên bản đuôi-đệ quy sau của hàm:
tail_sum_list([],Acc) ->
Acc;
tail_sum_list([Head|Tail],Acc) ->
tail_sum_list(Tail, Head + Acc).
biên dịch như:
{function, tail_sum_list, 2, 5}.
{label,4}.
{func_info,{atom,so},{atom,tail_sum_list},2}.
{label,5}.
{test,is_nonempty_list,{f,6},[{x,0}]}.
{get_list,{x,0},{x,2},{x,3}}.
{gc_bif,'+',{f,0},4,[{x,2},{x,1}],{x,1}}.
{move,{x,3},{x,0}}.
{call_only,2,{f,5}}.
{label,6}.
{test,is_nil,{f,4},[{x,0}]}.
{move,{x,1},{x,0}}.
return.
Thông báo việc thiếu allocate
và call_only
opcode trong mòn đuôi phiên bản đệ quy, trái ngược với allocate
/call
/deallocate
/return
chuỗi trong hàm không đệ quy.
Bạn không bị tràn ngăn xếp vì ngăn xếp Erlang "rất lớn. Thật vậy, tràn ngăn xếp thường có nghĩa là ngăn xếp bộ xử lý tràn, vì con trỏ ngăn xếp của bộ xử lý đã đi quá xa. Quy trình theo truyền thống có kích thước ngăn xếp hạn chế có thể được điều chỉnh bằng cách tương tác với hệ điều hành. Xem ví dụ của POSIX setrlimit.
Tuy nhiên, Erlang thực hiện chồng là không bộ vi xử lý ngăn xếp, vì mã được giải thích. Mỗi quá trình có ngăn xếp riêng của nó có thể phát triển khi cần thiết bằng cách gọi các hàm phân bổ bộ nhớ hệ điều hành (thường là malloc trên Unix).
Do đó, chức năng của bạn sẽ không bị lỗi miễn là malloc
cuộc gọi thành công.
Để lưu nội dung, danh sách thực tế L
đang sử dụng cùng một lượng bộ nhớ như ngăn xếp để xử lý. Thật vậy, mỗi phần tử trong danh sách có hai từ (chính giá trị số nguyên, được đóng thành một từ khi chúng nhỏ) và con trỏ đến phần tử tiếp theo trong danh sách. Ngược lại, ngăn xếp được phát triển bởi hai từ tại mỗi lần lặp bằng mã vạch allocate
: một từ cho CP
được tự lưu bởi allocate
và một từ theo yêu cầu (thông số đầu tiên là allocate
) cho giá trị hiện tại.
Đối với 100.000.000 từ trên máy ảo 64 bit, danh sách mất tối thiểu 1,5 GB (nhiều hơn khi chồng thực tế không được phát triển hai từ, may mắn thay). Việc giám sát và thu dọn này rất khó trong vỏ, vì nhiều giá trị vẫn còn tồn tại. Nếu bạn sinh ra một hàm, bạn có thể thấy mức sử dụng bộ nhớ:
spawn(fun() ->
io:format("~p\n", [erlang:memory()]),
L = lists:seq(1, 100000000),
io:format("~p\n", [erlang:memory()]),
sum_test:sum_list(L, 0),
io:format("~p\n", [erlang:memory()])
end).
Như bạn thấy, bộ nhớ cho cuộc gọi đệ quy không được giải phóng ngay lập tức.
Tôi vừa kiểm tra điều này và tôi nghĩ mình sai về không gian liên tục: ngăn xếp phát triển trong tất cả đệ quy cơ thể, nhưng ngăn xếp của Erlang hiệu quả hơn nhiều so với bạn có thể sử dụng. Việc tối ưu hóa mà tôi đã nghĩ đến là sự gia tăng mạnh mẽ trong * tốc độ *, không phải không gian, làm cho đệ quy cơ thể so sánh với sự đệ quy đuôi. Hãy thử thử nghiệm của bạn với một nghìn tỷ nguyên. – zxq9