[我正在阅读Learn Functional Programming with Elixir
,在第4章中,作者讨论了尾调用优化,即尾递归函数将使用比体递归函数更少的内存。但是,当我尝试书中的示例时,结果却相反。
# tail-recursive
defmodule TRFactorial do
def of(n), do: factorial_of(n, 1)
defp factorial_of(0, acc), do: acc
defp factorial_of(n, acc) when n > 0, do: factorial_of(n - 1, n * acc)
end
TRFactorial.of(200_000)
# body-recursive
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0, do: n * of(n - 1)
end
Factorial.of(200_000)
在我的计算机中,尾递归版本的beam.smp将使用2.5G〜3G内存,而身体递归仅使用1G左右。我误会了吗?
TL; DR: erlang虚拟机似乎将两者都优化为TCO。
如今,编译器和虚拟机太聪明了,无法预测其行为。 tail recursion的优点不是较少的内存消耗,而是:]]
这是为了确保不消耗系统资源,例如调用堆栈。
当调用不是尾递归时,必须在调用之间保留堆栈。请考虑以下示例。
。不会因为这种递归是无限的。编译器无法对其进行优化,这是我们得到的:▶ defmodule NTC do def inf do inf() IO.puts(".") DateTime.utc_now() end end
这里,我们需要保留堆栈,以便可以在递归返回时继续执行调用方
,这意味着我们将递归调用自身,直到堆栈耗尽。使用TCO,可以进行无限递归(并且广泛用于消息处理中。)▶ NTC.inf [1] 351729 killed iex
请注意,没有输出发生
回到您的示例。如我们所见,在两种情况下都进行了TCO(否则将导致堆栈溢出),前者将累加器保留在专用变量中,而后者仅使用堆栈的返回值。您会看到以下好处:elixir是不可变的,并且每次调用都会复制变量内容(对于200K阶乘而言这是巨大的),并保存在内存中。
侧注:
您可以使用:erts_debug.df(Factorial)
分解两个模块(这将在同一目录中产生Elixir.Factorial.dis
文件,并看到调用是隐式TCO的。