erlang尾递归更快么?

我们先看两段递归实现:
尾递归和其对应的汇编码:

tail_rec(_, [], Acc) -> lists:reverse(Acc); 
tail_rec(F, [H|T], Acc) -> tail_rec(F, T, [F(H)|Acc]). 
//注:虽然 beam_emu.c 中有 #define y(N) E[N]。 E[0]为进程空间的栈顶 
//但是加载器在加载到 {y,Y}时,会将Y加1。也就是说我们看汇编码中的Y(N),实际对应的地址为E[N+1] 
{label,4}.     
	{test,is_nonempty_list,{f,5},[{x,1}]}. //判断函数第二个传参是否为空,空则执行label5的逻辑     
	{allocate,3,3}.//栈区申请3+1个Eterm  即 E -= 4  注:剩余空间不够时会触发gc     
	{move,{x,2},{y,1}}.//E[2] = Acc     
	{move,{x,0},{y,2}}.//E[3] = F     
	{get_list,{x,1},{x,0},{y,0}}.//x(1)列表的第一个值赋值给x(0),尾部赋值给E[1]     
	{move,{y,2},{x,1}}.//x(1) = F     
	{call_fun,1}.//执行x(1), 即 F(H),传参和返回值都存在x(0)     
	{put_list,{x,0},{y,1},{x,2}}.// x(2) = [x(0) | E[2]]     
	{move,{y,0},{x,1}}.// E[1]赋值给x(1),即 x(1) = Acc     
	{move,{y,2},{x,0}}.// x(0) = F     
	{call_last,3,{f,4},3}.// E+=4;执行 label4 的逻辑。   
	//{call_last,3,{f,4},3} 意为 {指令名,目标函数参数,目标函数,释放空间M}   
	//加载器在加载到 call_last 指令时,会将M重写 NewM = (M+1)*sizeof(Eterm)   
	//在进行释放空间逻辑时,会释放掉 NewM/sizeof(Eterm) 个Eterm的空间   
	//进行下一次递归调用时,当前调用已不占用栈空间,从而形成尾递归   
{label,5}.     
	{move,{x,2},{x,0}}.     
	{call_ext_only,1,{extfunc,lists,reverse,1}}. //执行 lists:reverse(Acc)

非尾递归和其对应的汇编码:

body_rec(_, []) -> []; 
body_rec(F, [H|T]) -> [F(H) | body_rec(F, T)]. 

{label,7}.     
	{test,is_nonempty_list,{f,8},[{x,1}]}.//判断函数第二个传参是否为空,空则执行label8的逻辑     
	{allocate,2,2}.//栈区申请2+1个Eterm,即 E -= 3     
	{move,{x,0},{y,1}}.//E[2] = F     
	{get_list,{x,1},{x,0},{y,0}}.//x(1)列表的第一个值赋值给x(0),尾部赋值给E[1]     
	{move,{y,1},{x,1}}.//x(1) = F     
	{call_fun,1}.   
	//E[0]存放下条指令地址,执行x(1), 即 F(H),传参和返回值都存在x(0)   
	//F(H)执行完毕返回前,会执行 I = E[0];  E[0] = NIL;  Goto(*I)     
	{move,{y,0},{x,2}}.//x(2) = T     
	{swap,{y,1},{x,0}}.//x(0) = F,  E[2] = F(H)的返回值     
	{move,{x,2},{x,1}}.//x(1) = T     
	{trim,1,1}.//E += 1,  E[0] = NIL     
	{call,2,{f,7}}.//E[0] = 下条指令地址, 执行label7,     
	{put_list,{y,0},{x,0},{x,0}}.// x(0) = [E[1] 即 F(H)的返回值 | label7执行结果]     
	{deallocate,1}.//栈区释放2个Eterm空间  注:释放传参+1个,而不是传参个     
	return.//跳转到E[0]   
{label,8}.//函数第二个传参为空,则返回nil 即空列表     
	{test,is_nil,{f,6},[{x,1}]}.     
	{move,nil,{x,0}}.//x(0) 为NIL,即返回值为[]     
	return.//跳转到E[0]

两种写法的汇编码分析完毕后,我们就可以梳理流程并估算两种写法所需要空间大小:
尾递归写法:

判断Acc是否为空   
	是 →(运行1次)   
		翻转列表算法:   
			1.堆区剩余空间大于8则:利用堆剩余空间反转,直到空间不足时,2的逻辑   
			2.堆区剩余空间不足则: 会在堆外(p→mbuf)申请足够的空间   
	不是 → (运行N次)   
		栈区申请4个Eterm   
		执行F   
		堆区申请2个Eterm  
		栈区释放4个Eterm   
		函数跳转

可得:尾递归写法,每次递归调用时堆区占用2个Eterm,栈区不占用空间。递归结束时再一次性占用2N个Eterm的堆内剩余+堆外空间。
空间总占用为: 堆区4N Eterm
非尾递归写法:

判断Acc是否为空 
	是 → (运行1次)   
		函数跳转 
	不是 → (运行N次)   
		栈区申请3个Eterm   
		执行F   
		栈区释放1个Eterm   
		递归自身 (此时,当前调用会在栈区残留2个Eterm)   
		堆区申请2个Eterm   
		释放2个Eterm   
		函数跳转

可得:非尾递归写法,每次递归调用时栈区占用2个Eterm,当递归完毕返回时,每次返回会在堆区占用2个Eterm
空间总占用为:堆区 2N Eterm, 栈区 2N Eterm

接着对双方耗时进行测试:

//测试源码 
-module(recursion). 
-compile(export_all). 
tail_rec(F, L) -> tail_rec(F, L, []). 
tail_rec(_, [], Acc) -> lists:reverse(Acc); 
tail_rec(F, [H|T], Acc) -> tail_rec(F, T, [F(H)|Acc]). 

body_rec(_, []) -> []; 
body_rec(F, [H|T]) -> [F(H) | body_rec(F, T)]. 

f(X) -> X. 

main() ->
	PidB = spawn(fun F() -> 
		receive
			build ->
				put(list, lists:seq(1, 10000, 1)),
				io:format("build over~n"),
				F();
			start_body_gc -> 
				?MODULE:body_rec(fun f/1, get(list)),
				io:format("start_body_gc over:~n");
			start_tail_gc -> 
				?MODULE:tail_rec(fun f/1, get(list)),
				io:format("start_tail_gc over:~n");
			start_body_time ->
				List = [begin erlang:garbage_collect(), element(1, timer:tc(?MODULE, body_rec, [fun f/1, get(list)])) end || _ <- lists:seq(1,100,1)],
				io:format("start_body max_time:~p, min_time:~p, ave_time:~p~n", [lists:max(List), lists:min(List), lists:sum(List) div 100]);
			start_tail_time -> 
				List = [begin erlang:garbage_collect(), element(1, timer:tc(?MODULE, tail_rec, [fun f/1, get(list)])) end || _ <- lists:seq(1,100,1)],
				io:format("start_tail max_time:~p, min_time:~p, ave_time:~p~n", [lists:max(List), lists:min(List), lists:sum(List) div 100]);
			_ ->
				io:format("error~n")
		end 
	end),
	TraceB = spawn(fun trace/0),
	PidB!build,
	receive after 1000 -> skip end,
	io:format("begin trace~n"),
	erlang:trace(PidB, true, [garbage_collection, {tracer, TraceB}]),
	{PidB, TraceB}.

trace() ->
	receive 
		{_,_,gc_minor_start,Info} -> 
			io:format("gc_minor_start:~p~n", [Info]), 
			M = get(min), case M of undefined -> Nm = 0; _ -> Nm = M end, put(min, Nm+1), trace();
		{_,_,gc_major_start,Info} ->
			io:format("gc_major_start:~p~n", [Info]), 
			M = get(max), case M of undefined -> Nm = 0; _ -> Nm = M end, put(max, Nm+1), trace();
		print -> 
			io:format("~p~n",[{get(min), get(max)}]);
		_ ->trace() 
	end.

测试流程:

//测试双方空间足够的情况下,耗时和gc次数 
//注:测试操作系统:linux
//使用 erl +hms 80000 +S 1:1提供足够的内存空间测试 
{Pid, Trace} = recursion:main(). 
//测试耗时 
Pid!start_body_time.测试非尾递归耗时:start_body max_time:810, min_time:484, ave_time:502 

Pid!start_tail_time.测试尾递归耗时:start_tail max_time:765, min_time:486, ave_time:506 

//测试gc 
Pid!start_body_gc.测试非尾递归 
Pid!start_tail_gc.测试尾递归 
Trace!print.打印测试结果 
测试结果均为:{undefined,undefined} 
//可见空间足够的情况下,双方耗时相差无几
//测试erl +S 1:1正常启动的情况下,gc次数 
{Pid, Trace} = recursion:main(). 
Pid!start_body_gc. 测试非尾递归
Pid!start_tail_gc. 测试尾递归 
Trace!print. 
非尾递归结果均为:{3,undefined};  
尾递归结果:{3,1} 
//测试耗时需要调整代码 
//测试代码如下: 
time_test(Type) ->
	RecPid = spawn(fun time_receive/0),
	[begin receive after 100 -> skip end, spawn(?MODULE, time_run, [Type, RecPid]) end || _ <- lists:seq(1,100,1)].

time_run(body, RecPid) ->
	L = lists:seq(1, 10000, 1),
	RecPid ! element(1, timer:tc(?MODULE, body_rec, [fun f/1, L]));
time_run(tail, RecPid) ->
	L = lists:seq(1, 10000, 1),
	RecPid ! element(1, timer:tc(?MODULE, tail_rec, [fun f/1, L])).

time_receive() -> 
	receive
		Time when is_integer(Time) ->
			case get(list) of
				List when is_list(List) ->
					NewList = [Time]++List,
					case length(NewList) of
						100 -> 
							io:format("max_time:~p, min_time:~p, ave_time:~p~n", [lists:max(NewList), lists:min(NewList), lists:sum(NewList) div 100]);
						_ -> 
							put(list, NewList),
							time_receive()
					end;
				undefined ->
					put(list, [Time]),
					time_receive()
			end;
		_ ->
			error
	end.


//测试结果如下: 
recursion:time_test(body).    
打印结果:max_time:896, min_time:583, ave_time:604 

recursion:time_test(tail).    
打印结果:max_time:1463, min_time:861, ave_time:931

就结果看来,尾递归会多触发一次大gc,平均耗时也多过非尾递归。
那尾递归是怎么多出一次大gc的?可以根据gc规则进行推导:

测试进程在执行  put(list, lists:seq(1, 10000, 1)) 后,内存空间状态为 : 
堆栈总大小:17731; 堆区消耗:6464; 栈区消耗:0; 水位线:4192 
老堆消耗:13536; 老堆总大小:28690 

非尾递归gc次数来源:   
每次进入函数时,栈区申请3个Eterm,执行完F后栈区释放1个Eterm
  
此时栈区可用空间为:17731-6464 = 11267   
可支持 11267/2 = 5633次递归调用,第5634次调用时触发小gc    

第一次小gc触发后内存状态为:   
堆栈总大小:17731; 堆区消耗:2272; 栈区消耗:11267; 水位线: 2272   
老堆消耗:17728; 老堆总大小:28690     
此时栈区可用空间为:17731-2272-11267=4192   
也就是在:4192/2=2096次递归调用后,会触发第二次小gc     

第二次小gc触发后内存状态为:   
堆栈总大小:17731; 堆区消耗:0; 栈区消耗:15459; 水位线: 0   
老堆消耗:20000; 老堆总大小:28690     
此时栈区可用空间为:17731-15459=2272   
也就是在:2272/2=1136次递归调用后,会触发第三次小gc     

第三次小gc触发后内存状态为:   
堆栈总大小:28690; 堆区消耗:0; 栈区消耗:17731; 水位线: 0   
老堆消耗:20000; 老堆总大小:28690   

结论:非尾递归执行了3次小gc逻辑,一次空间增长逻辑   
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
尾递归gc次数来源:   
每次递归占用2个堆区Eterm   
此时堆区可用空间为:17731-6464=11267   
可支持 11267/2 = 5633次递归调用,第5634次调用时触发小gc   

第一次小gc触发后内存状态为:   
堆栈总大小:17731; 堆区消耗:13539; 栈区消耗:0; 水位线: 13539   
老堆消耗:17728; 老堆总大小:28690     
此时堆区可用空间为:17731-13539=41924192/2=2096次递归调用后,会触发第二次小gc,
小gc逻辑中(在实际gc逻辑前)发现老堆不够用,于是会触发大gc 

第一次大gc触发后内存状态为:   
堆栈总大小:46422; 堆区消耗:35459; 栈区消耗:0; 水位线: 35459   
老堆消耗:0; 老堆总大小:0     

执行lists:reverse操作时,由于是bif操作,会在操作结束后执行一次小gc     

结论:尾递归实际执行了2次小gc,一次大gc,一次空间增长逻辑

测试案例中,尾递归表现稍差的原因呼之欲出——大gc,可真是这样么?

如果没有这次大gc,双方的性能表现又会如何呢?
选5000,调整下测试逻辑

body_rec(_, []) -> [];  
改为  body_rec(_, [5000|Acc]) -> [5000|Acc]; 

tail_rec(_, [], Acc) -> lists:reverse(Acc);
改为 tail_rec(_, [5000|Acc1], Acc2) -> lists:reverse(Acc2)++[5000|Acc1]; 

recursion:time_test(body).  
结果:max_time:458, min_time:284, ave_time:301 

recursion:time_test(tail).  
结果:max_time:619, min_time:388, ave_time:421   
可见在均不触发大gc的情况下,非尾递归的表现还是更优,一个可能的原因是调用bif会触发一次小gc,
尾递归需要reverse的写法天然多了一次小gc消耗,那我们给非尾递归人为的加个小gc,再看看双方对比:   
body_rec(_, []) -> []; 
改为 body_rec(_, [5000|Acc]) -> erlang:garbage_collect(self(), [{type, 'minor'}]), [5000|Acc]; 

recursion:time_test(body). 
结果:max_time:532, min_time:315, ave_time:337 
还是比尾递归快

那尾递归就赢不了么,天无绝尾递归之路,如下例子尾递归就更快些:

spawn(fun() ->     
	L = lists:seq(1,250000),     
	F = fun(X) -> X end,     
	{{A,_}, {B,_}} = {timer:tc(recursion, body_rec, [F, L]), timer:tc(recursion, tail_rec, [F, L])},     
	io:format("Body:~p~nTail:~p~n", [A,B]) end). 

结果打印: Body:9391 Tail:4303

问题来了…这是为什么呢(奸笑)?
有如下函数:

//注 使用erl 23版本,别的版本产生如下效果需要的数值需另行探讨
single_tail(N) -> 
	spawn(fun() -> io:format("~p~n", [element(1, timer:tc(recursion, tail_rec, [fun(X)-> X end, lists:seq(1,N,1)]))]) end).   

测试打印: 
recursion:single_tail(3500000).  打印: 554862 
recursion:single_tail(3800000).  打印: 498627

列表更长,耗时反而更短…这又是为什么呢?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值