长生不老药:为什么尾递归比体递归函数使用更多的内存?

elixir: why tail-recursive use more memory than a body-recursive function?

我正在阅读 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内存,而body-recursive只使用1G左右。我是不是误会了什么?

TL;DR: 虚拟机似乎都优化了 TCO。

现在的编译器和虚拟机太聪明了,无法预测它们的行为。 tail recursion的好处不是少内存消耗,而是:

This is to ensure that no system resources, for example, call stack, are consumed.

当调用不是尾递归时,必须在调用之间保留堆栈。考虑以下示例。

▶ defmodule NTC do
    def inf do
      inf()
      IO.puts(".")
      DateTime.utc_now()
    end
  end

这里我们需要保留堆栈,以便在递归 return 时可以继续执行 调用程序 。不会的,因为这个递归是无限的。编译器无法优化它,这是我们得到的结果:

▶ NTC.inf                                                                    
[1]    351729 killed     iex

请注意,没有输出发生,这意味着我们递归调用自身直到堆栈爆炸。使用 TCO,无限递归是可能的(并且广泛用于消息处理。)


回到你的例子。正如我们所见,两种情况下都产生了 TCO(否则我们最终会出现堆栈溢出),前者在专用变量中保留一个累加器,而后者仅使用堆栈上的 return 值。你看到的这个增益: 是不可变的,变量内容(对于 200K 的阶乘来说是巨大的)被复制并保存在每次调用的内存中。


旁注:

您可以使用 :erts_debug.df(Factorial) 反汇编这两个模块(这将在同一目录中生成 Elixir.Factorial.dis 文件)并看到这些调用是隐式的 TCO。