内存管理-为什么纯函数式语言不使用引用计数?

在纯功能语言中,数据是不可变的。 使用参考计数时,创建参考周期需要更改已创建的数据。 似乎纯函数式语言可以使用引用计数,而不必担心循环的可能性。 是吗 如果是这样,为什么不呢?

我知道在很多情况下引用计数都比GC慢,但至少可以减少暂停时间。 如果暂停时间不好,可以选择使用引用计数。

6个解决方案
26 votes

相对于其他托管语言(如Java和C#),纯函数式语言疯狂分配。 他们还分配不同大小的对象。 最快的已知分配策略是从连续的空闲空间(有时称为“托儿所”)进行分配,并保留硬件寄存器以指向下一个可用的空闲空间。 堆中的分配变得与堆栈中的分配一样快。

引用计数与该分配策略根本不兼容。 引用计数会将对象放在空闲列表中,然后将其再次取走。 引用计数还具有在创建新对象时更新引用计数所需的大量开销(如上所述,纯函数语言确实很疯狂)。

在以下情况下,引用计数往往会做得很好:

  • 几乎所有堆内存都用于保存活动对象。
  • 相对于其他操作,分配和指针分配很少。
  • 引用可以在其他处理器或计算机上进行管理。

要了解当今最好的高性能参考计数系统是如何工作的,请查阅David Bacon和Erez Petrank的工作。

Norman Ramsey answered 2020-08-03T18:18:20Z
19 votes

您的问题是基于错误的假设。 具有循环引用和不可变数据是完全可能的。 考虑下面的C#示例,该示例使用不可变数据创建循环引用。

class Node { 
  public readonly Node other;
  public Node() { 
    other = new Node(this);
  }
  public Node(Node node) {
    other = node;
  }
}

这种技巧可以用许多功能语言完成,因此任何收集机制都必须处理循环引用的可能性。 我并不是说引用计数机制对于循环引用是不可能的,只是必须加以处理。

由ephemient编辑

回应评论...在Haskell中这是微不足道的

data Node a = Node { other :: Node a }
recursiveNode = Node { other = recursiveNode }

在SML上几乎没有付出更多的努力。

datatype 'a node = NODE of unit -> 'a node
val recursiveNode : unit node =
    let fun mkRecursiveNode () = NODE mkRecursiveNode
    in mkRecursiveNode () end

无需突变。

JaredPar answered 2020-08-03T18:17:32Z
10 votes

我认为有几件事。

  • 有循环:许多语言中的“ let rec”确实允许创建“圆形”结构。 除此之外,不变性通常并不意味着没有周期,但这违反了规则。
  • 引用计数不利于列表:例如,我不知道引用计数集合可以很好地工作。 您经常在FP中发现的长单链列表结构(例如,慢速,需要确保尾递归等)
  • 其他策略也有好处:正如您所提到的,其他GC策略通常对于内存局部性仍然更好

(曾几何时,我认为我可能真的'知道'了这一点,但是现在我试图记住/推测,所以不要以任何权威为准。)

Brian answered 2020-08-03T18:18:58Z
8 votes

考虑一下关于Lisp机器的发明者David Moon的寓言:

有一天,一个学生来到月球说:“我知道如何做一个更好的垃圾收集器。我们必须保留指向每个缺点的指针的引用计数。”

Moon耐心地向学生讲了以下故事:

“有一天,一个学生来到月球说:'我知道如何做一个更好的垃圾收集器...

Crashworks answered 2020-08-03T18:19:31Z
8 votes

是吗

不完全的。 您可以使用纯函数式编程来创建循环数据结构,只需同时定义相互递归的值即可。 例如,在OCaml中:

let rec xs = 0::ys and ys = 1::xs

但是,可以定义无法通过设计创建循环结构的语言。 结果称为单向堆,其主要优点是垃圾收集可以像引用计数一样简单。

如果是这样,为什么不呢?

一些语言确实禁止循环并使用引用计数。 Erlang和Mathematica是示例。

例如,在Mathematica中,当您引用一个值时,会对其进行深拷贝,因此,对原始值进行更改不会对副本进行更改:

In[1] := xs = {1, 2, 3}
Out[1] = {1, 2, 3}

In[2] := ys = xs
Out[2] = {1, 2, 3}

In[3] := xs[[1]] = 5
Out[3] = 5

In[4] := xs
Out[4] = {5, 2, 3}

In[5] := ys
Out[5] = {1, 2, 3}
Jon Harrop answered 2020-08-03T18:20:14Z
-4 votes

引用计数比GC慢得多,因为它对CPU不利。 GC大部分时间可以等待空闲时间,并且GC可以是并发的(在另一个线程上)。 这就是问题所在-GC最不邪恶,很多尝试都证明了这一点。

Mash answered 2020-08-03T18:20:34Z
translate from https://stackoverflow.com:/questions/791437/why-dont-purely-functional-languages-use-reference-counting