elixir:为什么尾递归使用的内存比身体递归函数更多?

问题描述 投票:0回答:1

[我正在阅读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左右。我误会了吗?

recursion elixir
1个回答
0
投票

TL; DR: 虚拟机似乎将两者都优化为TCO。

如今,编译器和虚拟机太聪明了,无法预测其行为。 tail recursion的优点不是较少的内存消耗,而是:]]

这是为了确保不消耗系统资源,例如调用堆栈。

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

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

这里,我们需要保留堆栈,以便可以在递归返回时继续执行调用方

。不会因为这种递归是无限的。编译器无法对其进行优化,这是我们得到的:
▶ NTC.inf                                                                    
[1]    351729 killed     iex

请注意,没有输出发生

,这意味着我们将递归调用自身,直到堆栈耗尽。使用TCO,可以进行无限递归(并且广泛用于消息处理中。)

回到您的示例。如我们所见,在两种情况下都进行了TCO(否则将导致堆栈溢出),前者将累加器保留在专用变量中,而后者仅使用堆栈的返回值。您会看到以下好处:是不可变的,并且每次调用都会复制变量内容(对于200K阶乘而言这是巨大的),并保存在内存中。


侧注:

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

© www.soinside.com 2019 - 2024. All rights reserved.