Erlang 尾部递归与非尾部递归。前者慢吗?

Erlang 尾部递归与非尾部递归。前者慢吗?,erlang,tail-recursion,Erlang,Tail Recursion,我正在学习函数编程和Erlang的基础知识,并实现了阶乘函数的三个版本:使用带保护的递归、使用带模式匹配的递归和使用尾部递归 我试图比较每个阶乘实现Erlang/OTP 22[erts-10.4.1]的性能: %%简单阶乘码: N==0->1时的facN; 当N>0->N*facN-1时的facN。 %%使用模式匹配: fac模式匹配0->1; 当N>0->N*fac\u模式匹配N-1时,fac\u模式匹配N。 %%使用尾部递归和模式匹配: tail\u facN->tail\u facN,1。

我正在学习函数编程和Erlang的基础知识,并实现了阶乘函数的三个版本:使用带保护的递归、使用带模式匹配的递归和使用尾部递归

我试图比较每个阶乘实现Erlang/OTP 22[erts-10.4.1]的性能:

%%简单阶乘码: N==0->1时的facN; 当N>0->N*facN-1时的facN。 %%使用模式匹配: fac模式匹配0->1; 当N>0->N*fac\u模式匹配N-1时,fac\u模式匹配N。 %%使用尾部递归和模式匹配: tail\u facN->tail\u facN,1。 尾部fac0,Acc->Acc; tail\u facN,N>0时的Acc->tail\u facN-1,N*Acc。 计时器帮助程序:

-定义精度,微秒。 执行时间m,F,A,D-> StartTime=erlang:系统时间?精度, 结果=应用ym,F,A, EndTime=erlang:系统时间?精度, io:formatExecution需要~p~ps~n、[EndTime-StartTime,?精度], 如果 D=:=true->io:formatResult是~p~n,[Result]; 正确->确定 终止 . 执行结果:

递归版本:

3> mytimer:execution\u timefactorial,fac,[1000000],false。 执行时间为1253949667微秒 好啊 递归模式匹配版本:

4> mytimer:execution\u timefactorial,fac\u pattern\u matching,[1000000],false。 执行耗时1288239853微秒 好啊 尾部递归版本:

5> mytimer:execution\u timefactorial,tail\u fac,[1000000],false。 执行时间为1405612434微秒 好啊 我原本期望尾部递归版本比其他两个版本的性能更好,但令我惊讶的是,它的性能更低。这些结果与我的预期完全相反


为什么?

问题在于您选择的函数。阶乘是一个增长非常快的函数。Erlang实现了大整数算法,因此不会溢出。您正在有效地衡量底层大整数实现的好坏。1000000! 这是一个巨大的数字。它是8.26×10^5565708,相当于5.6MB长的十进制数。您的fac/1和tail_fac/1之间存在差异,即在大整数实现开始时,它们达到大数字的速度有多快,以及数字增长的速度有多快。在您的fac/1实现中,您有效地计算了1*2*3*4*..*N。在您的尾部fac/1实现中,您计算的是N*N-1*N-2*N-3*..*1。你看到问题了吗?您可以用不同的方式编写尾部调用实现:

tail_fac2(N) when is_integer(N), N > 0 ->
    tail_fac2(N, 0, 1).

tail_fac2(X, X, Acc) -> Acc;
tail_fac2(N, X, Acc) ->
    Y = X + 1,
    tail_fac2(N, Y, Y*Acc).
它会工作得更好。我不像你那样有耐心,所以我会测量一个更小的数字,但新的事实是:tail_fac2/1应该比事实:fac/1每次都好:

1> element(1, timer:tc(fun()-> fact:fac(100000) end)).
7743768
2> element(1, timer:tc(fun()-> fact:fac(100000) end)).
7629604
3> element(1, timer:tc(fun()-> fact:fac(100000) end)).
7651739
4> element(1, timer:tc(fun()-> fact:tail_fac(100000) end)).
7229662
5> element(1, timer:tc(fun()-> fact:tail_fac(100000) end)).
7104056
6> element(1, timer:tc(fun()-> fact:tail_fac2(100000) end)).
6491195
7> element(1, timer:tc(fun()-> fact:tail_fac2(100000) end)).
6506565
8> element(1, timer:tc(fun()-> fact:tail_fac2(100000) end)).
6519624
正如您所看到的,N=100000的tail_fac2/1需要6.5秒,fact:tail_fac/1需要7.2秒,fac/1需要7.6秒。甚至更快的增长也不会颠覆尾部调用的好处,所以尾部调用版本比身体递归版本更快。事实上,可以清楚地看到,累加器的缓慢增长:tail_fac2/1显示了它的影响

如果您为尾部调用优化测试选择不同的函数,您可以更清楚地看到尾部调用优化的影响。例如sum:

sum(0) -> 0;
sum(N) when N > 0 -> N + sum(N-1).

tail_sum(N) when is_integer(N), N >= 0 ->
    tail_sum(N, 0).

tail_sum(0, Acc) -> Acc;
tail_sum(N, Acc) -> tail_sum(N-1, N+Acc).
速度是:

1> element(1, timer:tc(fun()-> fact:sum(10000000) end)).
970749
2> element(1, timer:tc(fun()-> fact:sum(10000000) end)).
126288
3> element(1, timer:tc(fun()-> fact:sum(10000000) end)).
113115
4> element(1, timer:tc(fun()-> fact:sum(10000000) end)).
104371
5> element(1, timer:tc(fun()-> fact:sum(10000000) end)).
125857
6> element(1, timer:tc(fun()-> fact:tail_sum(10000000) end)).
92282
7> element(1, timer:tc(fun()-> fact:tail_sum(10000000) end)).
92634
8> element(1, timer:tc(fun()-> fact:tail_sum(10000000) end)).
68047
9> element(1, timer:tc(fun()-> fact:tail_sum(10000000) end)).
87748
10> element(1, timer:tc(fun()-> fact:tail_sum(10000000) end)).
94233
正如你所看到的,在那里我们可以很容易地使用N=10000000,而且它的工作速度非常快。无论如何,body递归函数的速度明显低于110ms和85ms。您可以注意到第一次运行的事实:sum/1比其他运行花费的时间长9倍。这是因为体递归函数消耗堆栈。当您使用尾部递归对应项时,将不会看到这种效果。试试看。如果在单独的过程中运行每个度量,则可以看到差异

1> F = fun(G, N) -> spawn(fun() -> {T, _} = timer:tc(fun()-> fact:G(N) end), io:format("~p took ~bus and ~p heap~n", [G, T, element(2, erlang:process_info(self(), heap_size))]) end) end.
#Fun<erl_eval.13.91303403>
2> F(tail_sum, 10000000).
<0.88.0>
tail_sum took 70065us and 987 heap
3> F(tail_sum, 10000000).
<0.90.0>
tail_sum took 65346us and 987 heap
4> F(tail_sum, 10000000).
<0.92.0>
tail_sum took 65628us and 987 heap
5> F(tail_sum, 10000000).
<0.94.0>
tail_sum took 69384us and 987 heap
6> F(tail_sum, 10000000).
<0.96.0>
tail_sum took 68606us and 987 heap
7> F(sum, 10000000).
<0.98.0>
sum took 954783us and 22177879 heap
8> F(sum, 10000000).
<0.100.0>
sum took 931335us and 22177879 heap
9> F(sum, 10000000).
<0.102.0>
sum took 934536us and 22177879 heap
10> F(sum, 10000000).
<0.104.0>
sum took 945380us and 22177879 heap
11> F(sum, 10000000).
<0.106.0>
sum took 921855us and 22177879 heap

Erlang文档指出

It is generally not possible to predict whether the tail-recursive 
or the body-recursive version will be faster. Therefore, use the version that
makes your code cleaner (hint: it is usually the body-recursive version).

编写基准很难。你确定你在测量你认为你在测量的东西吗?你考虑过统计效应吗?您考虑过动态自适应优化吗?你考虑过环境问题吗?这里有几个例子,说明在基准测试中需要考虑哪些不明显的事情:。这些主要讨论热点,但这些问题大多适用于任何具有动态自适应优化的现代高性能执行引擎。您忽略了一个重要部分:根据神话,使用反向构建列表的尾部递归函数,然后调用list:reverse/1比按正确顺序构建列表的主体递归函数快;原因是体递归函数比尾部递归函数使用更多的内存。我刚才引用了文档。还请注意,通常不可能预测。。你引用了提到**的话,根据神话**,你把这句话删去了:在R12B之前,这在某种程度上是正确的。在R7B之前更是如此。今天,没有那么多。主体递归函数通常使用与尾部递归函数相同的内存量不,你不明白。整个2.3节都是关于一个函数,该函数反向构建一个列表,然后调用lists:reverse/
1.因此,如果您不构建列表或不调用列表:reverse/1您的主体递归函数通常不会使用与尾部递归函数相同的内存量,因此您可以100%确定地预测尾部递归函数将更有效。所以您的答案与OP无关,因为OP既不生成列表也不调用列表:reverse/1。试试看!反驳它!什么列表:用C写的NIF和上面的例子有什么关系?没什么,这就是为什么你的答案是不相关的。我第四次告诉你!您引用的谬论反驳是关于尾部递归函数的,该函数反向构建一个列表,然后调用list:reverse/1`它与上面的示例无关。这就是为什么你的答案是无关的,错误的和误导的!