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

Zif*_*fre 43 garbage-collection functional-programming memory-management reference-counting purely-functional

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

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

Nor*_*sey 26

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

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

在这样的情况下,引用计数往往表现得非常好:

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

要了解当今最好的高性能计数系统如何工作,请查看David BaconErez Petrank的工作.

  • 实际上可以在引用计数收集器中添加一个托儿所:http://cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf (7认同)
  • 混合收藏家绝对是未来,如果有人能够做到这一点,史蒂夫布莱克本可以(尤其是当他得到Perry Cheng的帮助时).也就是说,我不确定我是否愿意将他们的Java结果推断为现代功能语言.Java在内存系统上并不太难. (3认同)

Jar*_*Par 19

您的问题基于错误的假设.完全可能有循环引用和不可变数据.请考虑以下使用不可变数据创建循环引用的C#示例.

class Node { 
  public readonly Node other;
  public Node() { 
    other = new Node(this);
  }
  public Node(Node node) {
    other = node;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

ephemient编辑

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

data Node a = Node { other :: Node a }
recursiveNode = Node { other = recursiveNode }
Run Code Online (Sandbox Code Playgroud)

在SML中几乎没有任何努力.

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

不需要突变.

  • `你的例子仍然依赖于修改已经创建的数据(尽管在构造函数中).` ... false claim.该例子中仍然没有突变. (9认同)
  • 您的示例仍然依赖于修改已创建的数据(尽管在构造函数中).我不知道这在任何纯功能语言中都是可能的.你能在Haskell中给出一个简单的例子(如果可能的话)吗? (2认同)
  • 数据持久性阻止循环引用...你在Haskell中的例子是无限递归.只需拆卸它就会看到......开始思考功能并停止传播错误信息...... (2认同)

Bri*_*ian 10

我想有几件事.

  • 周期:"让REC"在许多语言中也允许创建"循环"结构.除此之外,不变性通常意味着没有周期,但这违反了规则.
  • 列表中的引用计数很糟糕:我不知道引用计数集合适用于您经常在FP中找到的长单链表结构(例如,缓慢,需要确保尾递归,...)
  • 其他策略也有好处:正如您所提到的,其他GC策略通常对内存局部性更好

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

  • 只要你懒惰地执行它,引用计数就可以正常工作:当一个对象引用计数变为零时,它会进入空闲列表,但是你不会减少它指向的东西,直到它从空闲列表中删除.这样每个操作都是恒定时间(由最大对象的大小限制,而不是最长列表的长度). (7认同)

Cra*_*rks 8

考虑一下这个寓言,讲述了Lisp机器的发明者David Moon:

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

Moon耐心地告诉学生以下故事:

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


Jon*_*rop 8

我是对的?

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

let rec xs = 0::ys and ys = 1::xs
Run Code Online (Sandbox Code Playgroud)

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

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

有些语言会禁止循环并使用引用计数.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}
Run Code Online (Sandbox Code Playgroud)

  • +1表示"单向堆".这是Google需要您了解有关此主题的更多信息.:) (2认同)