为什么纯函数式语言不使用引用计数?

时间:2009-04-26 19:24:54

标签: memory-management functional-programming garbage-collection reference-counting purely-functional

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

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

6 个答案:

答案 0 :(得分:26)

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

引用计数从根本上与此分配策略不兼容。引用计数将对象放在空闲列表中并再次取消它们。在创建新对象时,引用计数也需要大量的开销来更新引用计数(如上所述,纯函数式语言就像疯了一样)。

参考计数往往在以下情况下表现良好:

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

要了解当今最佳高性能参考计数系统的工作原理,请查看David BaconErez Petrank的工作。

答案 1 :(得分:19)

您的问题基于错误的假设。完全可能有循环引用和不可变数据。请考虑以下使用不可变数据创建循环引用的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

不需要突变。

答案 2 :(得分:10)

我认为有一些事情。

  • 周期:许多语言中的“let rec”确实允许创建“循环”结构。除此之外,不变性通常意味着没有周期,但这违反了规则。
  • 列表中的参考计数不好:我不知道参考计数的集合适用于例如您经常在FP中找到的长单链表结构(例如,缓慢,需要确保尾递归,......)
  • 其他策略有好处:正如您所说,其他GC策略通常仍然更适合内存位置

(曾几何时,我想我可能真的'知道'这个,但现在我想记住/推测,所以不要把它视为任何权威。)

答案 3 :(得分:8)

考虑this allegory讲述David Moon的发明者Lisp Machine

  

有一天,一个学生来到Moon并说道:“我理解如何制作一个更好的垃圾收集器。我们必须保留对每个缺点的指针的引用计数。”

     月亮耐心地告诉学生以下的故事:

     
    

“有一天,一个学生来到Moon说:'我明白如何制作一个更好的垃圾收集器......

  

答案 4 :(得分:8)

  

我是对的吗?

不完全。您可以使用纯函数式编程创建循环数据结构,只需同时定义相互递归的值即可。例如,在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}

答案 5 :(得分:-4)

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