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

时间:2015-01-04 01:04:58

标签: garbage-collection scheme lisp racket

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

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

函数式编程的核心习惯是你设计函数来处理数据的副本,并返回处理后的数据,而不是远程改变数据结构。你不必担心额外的复制,因为垃圾收集器来了并恢复了不再使用的内存。

这就是它的伟大之处。但是当我开始编写处理更大数据结构的程序时,我发现垃圾收集占用了更多的总运行时间(比如25-35%,而不是我发现的10-15%)小结构)。

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

但它也使速度提升变得更加困难,因为垃圾收集器会占用更多的运行时间,而垃圾收集器基本上不在您的控制范围之内。

显而易见的解决方案是避免复制数据。但是这会使你重新从远处变异数据,这与功能成语相矛盾。 (虽然我知道这可以在Racket中通过使用盒装值,甚至参数来完成。)

除此之外,我还有三种选择:

  1. 设计您的数据结构,以便更紧凑地编码信息。
  2. 不是将整个数据结构传递给函数,而是提取您需要处理的数据子集(当然,假设分离和重新加入数据子集的成本节省了足够的垃圾收集时间,值得)。
  3. 在可能的情况下,将多个函数(通常将大数据结构从一个传递到下一个,每次复制它)组合成一个单尾递归函数(不会)。
  4. 这些有效的方法吗?我还有其他人可以忽略吗?

3 个答案:

答案 0 :(得分:4)

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

克里斯·冈崎的Purely Functional Data Structures是我所知道的最好的论文,但最近可能还有一些我不知道的研究(例如,ctrie,我只通过维基百科知道。

答案 1 :(得分:1)

这不是一个完美的答案,但它比我发现的其他资源更具主题。本文“Typed Racket中的功能数据结构”讨论了在某些情况下比标准列表表现更好的替代功能数据结构,并给出了包含垃圾收集时间的特定时序结果(HT Greg Hendershott):

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

这是实现的代码:

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

答案 2 :(得分:0)

产生大量不必要的垃圾当然是坏事。如果探查器显示您正在分配大量内存,并且GC正在成功恢复大量内存,那么您应该考虑减少垃圾。这是你(1)进来的地方:

  

设计您的数据结构,以便更紧凑地编码信息。

这一般来说非常重要。如果GC时间对性能造成影响,请尝试使用更紧凑的数据结构。如果GC时间会影响性能,请尝试使用更紧凑的数据结构。紧凑的数据结构提高了GC性能和缓存利用率。如果您可以用更少的字节将更多信息填充到CPU缓存中,那么通常可以显着提高速度。

另一方面,你的(2)和(3)看起来很腥,并向我建议你可能不清楚如何在记忆中实际表示价值。将结构传递给函数通常将其复制到典型的函数式语言实现中。也就是说,"拉出你需要处理的数据子集"可以有意义,因为如果不需要其余部分,你可以更快地把它变成垃圾,这很好。

最后一点是25-35%的GC时间可能听起来不错,但它并不像它那样糟糕。我看到的最糟糕的情况是“弱世代假设”#34;被侵犯了。也就是说,当你分配一堆内存时, not 会很快变成垃圾。这很糟糕,因为垃圾收集器不断尝试收集垃圾,追踪越来越多的非垃圾,而实际上并没有实现太多。在这些情况下,我个人看到GC时间高达75%左右,但如果情况可能更糟,我不会感到惊讶。

相关问题