在函数式语言中使用大型数据结构时减少垃圾收集时间

Reducing garbage-collection time while using large data structures in a functional language

在函数式语言中使用大型数据结构时如何减少垃圾收集时间?

(我使用的是 Racket,但这个问题适用于任何带有垃圾收集器的面向功能的语言。)

函数式编程的核心习语是您设计函数来处理数据的副本,return 处理后的数据,而不是从远处改变数据结构。您不必担心额外的复制,因为垃圾收集器会过来回收不再使用的内存。

就目前而言,这很棒。但是当我开始编写处理更大数据结构的程序时,我发现更多的总运行时间被垃圾收集占用(25-35%,而不是我发现的典型小结构的 10-15% ).

这并不奇怪,因为每次函数调用都会复制更多数据,因此需要收集更多垃圾。

但这也使速度提高变得更加困难,因为垃圾收集器占用了更多的运行时间,这基本上是您无法控制的。

显而易见的解决方案是完全避免复制数据。但这会让你从远处回到变异数据,这与函数习语相矛盾。 (虽然我知道这可以在 Racket 中通过使用盒装值甚至参数来在某种程度上完成。)

除此之外,我想到了三个选项:

  1. 设计您的数据结构,使它们对信息进行更紧凑的编码。
  2. 与其将整个数据结构传递给函数,不如提取您需要处理的数据子集(当然,假设分离和重新加入数据子集的成本节省了足够的垃圾收集时间是值得的) .
  3. 在可能的情况下,将多个函数(通常会将大型数据结构从一个传递到另一个,每次都复制它)组合成一个尾递归函数(不会)。

这些方法有效吗?还有其他我忽略的吗?

有一些功能性数据结构旨在降低复制成本 - 例如,如果一个函数改变了树的一个分支,那么新树将共享未受影响的分支的节点,并且只共享突变的分支需要复制。

Chris Okasaki 的 Purely Functional Data Structures is the best paper on this that I am aware of, but there is probably more recent research that I am unaware of (for example, the ctrie,我只通过维基百科知道)。

这不是一个完美的答案,但它比我找到的其他资源更切题。这篇论文“Typed Racket 中的函数式数据结构”讨论了在某些情况下比标准列表表现更好的替代函数式数据结构,并给出了包括垃圾收集时间在内的具体计时结果(HT Greg Hendershott):

http://www.ccs.neu.edu/racket/pubs/sfp10-kth.pdf

下面是实现代码:

https://github.com/takikawa/tr-pfds

产生大量不必要的垃圾当然不好。如果分析器显示您正在分配大量内存,并且 GC 成功回收了大量内存,您应该考虑减少产生垃圾。那是您的 (1) 出现的地方:

Design your data structures so that they encode information more compactly.

总的来说,这非常重要。如果 GC 时间影响性能,请尝试使用更紧凑的数据结构。如果 GC 时间 不会 影响性能,请尝试使用更紧凑的数据结构。紧凑的数据结构提高了 GC 性能和缓存利用率。如果您能以更少的字节将更多信息塞入 CPU 缓存,您通常可以获得显着的速度提升。

另一方面,您的 (2) 和 (3) 看起来很可疑,向我建议您可能不清楚值在内存中的实际表示方式。将结构传递给函数 而不是 通常会在典型的函数式语言实现中复制它。也就是说,"pull out the subset of data you need to process"是有道理的,因为如果剩下的可能不需要,你可以更快地把它变成垃圾,这很好。

最后一件事是 25-35% 的 GC 时间可能听起来有点糟糕,但它并没有想象中那么糟糕。我见过的最糟糕的情况是 "weak generational hypothesis" 被违反了。也就是说,当您分配一堆 而不是 的内存时,它们很快就会变成垃圾。这很糟糕,因为垃圾收集器一直在尝试收集垃圾,追踪越来越多的非垃圾,但实际上并没有完成多少工作。在这些情况下,我亲眼看到 GC 时间高达 75% 左右,但如果比这更糟,我也不会感到惊讶。