【问题标题】:Why don't purely functional languages use reference counting?为什么纯函数式语言不使用引用计数?
【发布时间】:2010-10-21 22:38:57
【问题描述】:

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

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

【问题讨论】:

  • 引用计数其实是一种垃圾回收算法。所以 RC vs GC 没有任何意义。引用计数与标记清除或压缩收集器是一个更好的问题。
  • 正如 Brian 和 JaredPar 所说,可以创建没有可变性的循环结构。事实上,我想说它们在 Haskell 中比任何其他语言都更多出现,这要归功于它们易于处理(万岁懒惰)。 JaredPar 的答案中已添加示例。
  • @Luke Quinane:当我说 GC 时,大多数人不会想到 RC。 GC 通常被认为是指 mark-swep-like 策略。
  • @Zifre:当您说“垃圾收集”时,大多数人会想到来收拾垃圾的卡车。我不认为人口流行度的论点与该主题真正密切相关。
  • @chaos,肯定有密切相关的人口统计数据是相关的吗?

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


【解决方案1】:

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

引用计数与这种分配策略根本不兼容。引用计数将对象放在空闲列表中,然后再次将它们删除。引用计数还需要在创建新对象时更新引用计数所需的大量开销(如上所述,纯函数式语言确实如此疯狂)。

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

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

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

【讨论】:

  • 其实可以在引用计数收集器中添加nursery:cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf
  • 混合收藏家绝对是未来,如果有人能做到这一点,史蒂夫布莱克本可以(尤其是当他得到 Perry Cheng 的帮助时)。也就是说,我不确定我是否愿意将他们的 Java 结果外推到现代函数式语言中。 Java 在内存系统上并不太难。
【解决方案2】:

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

不需要突变。

【讨论】:

  • 您的示例仍然依赖于修改已创建的数据(尽管在构造函数中)。不过,我不知道这在任何纯函数式语言中都是可能的。你能在 Haskell 中举一个简单的例子吗(如果可能的话)?
  • @ephemient,谢谢你的例子。我对 Haskell 有点陌生,所以请原谅我问了这么一个微不足道的问题。
  • Your example still relies on modifying already created data (albeit in the constructor).... 虚假声明。该示例中仍然没有突变。
  • 数据持久化防止循环引用......您在 Haskell 中的示例是无限递归。只需拆开它,你就会看到......开始从功能上思考并停止传播错误信息......
  • 实际上,当实现循环的唯一方法是创建时,引用计数可以正常工作。只需使用共享引用计数器创建对象即可。
【解决方案3】:

我认为有几件事。

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

(曾几何时,我想我可能真的“知道”这一点,但现在我试图记住/推测,所以不要把它当作任何权威。)

【讨论】:

  • 引用计数适用于列表,只要你懒惰地这样做:当一个对象引用计数变为零时,它会进入空闲列表,但你不会减少它指向的东西,直到它脱离了免费列表。这样,每个操作都是恒定时间(受最大对象的大小限制,而不是最长列表的长度)。
  • 让 rec 是引入共享引用计数器的精确位置
  • @OCTAGRAM 你的意思是共享的,比如 C++ 的 std::shared_ptr,还是弱的,比如 C++ 的 std::weak_ptr
  • @MaxBarraclough 计数器在对象之间共享,而不仅仅是在引用之间。更像是 TAggregatedObject 将 AddRef 和 Release 重定向到它的 master
【解决方案4】:

我是对的吗?

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

【讨论】:

  • +1 表示“单向堆”。这是您需要通过 Google 了解更多有关此主题的内容。 :)
【解决方案5】:

考虑this allegory 讲述了David MoonLisp Machine 的发明者:

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

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

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

【讨论】:

    【解决方案6】:

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

    【讨论】:

      猜你喜欢
      • 2020-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-12
      • 1970-01-01
      • 2020-06-13
      • 2011-04-01
      • 1970-01-01
      相关资源
      最近更新 更多