如何在 Haskell 中查找和修复由于 GC 导致的性能问题?

Posted

技术标签:

【中文标题】如何在 Haskell 中查找和修复由于 GC 导致的性能问题?【英文标题】:How to find and fix performance issues due to GC in Haskell? 【发布时间】:2014-01-01 02:27:55 【问题描述】:

我有一个用 Haskell 编写的小程序,用于在 Boggle 拼图中查找单词(code here、blog post here)。我最近做了一部分代码,深度优先搜索,并行使用Control.Parallel.Strategies。使用-N2 运行程序会加快程序速度,但超过两个线程会降低性能。在-N8,该程序比顺序版本慢了大约 4 倍。

查看+RTS -s 的输出和 ThreadScope 中的线程行为,很明显垃圾收集是罪魁祸首,因为它占用了高达 45% 的执行时间。既然我知道了这一点,我如何才能找到最有可能是罪魁祸首的函数,Haskell 中是否有修复此类泄漏的一般提示或技术?

【问题讨论】:

是慢速的 GC,还是等待 stop-the-world GC 运行时核心空闲?从 ThreadScope 图表中应该很容易看出这一点。 【参考方案1】:

这里最简单的优化是将 trie 查找移动到 parMap 中。它是所有已完成工作的重要组成部分,因此按顺序执行会极大地损害缩放。我们可以使用以下函数代替findWords

process :: Dict -> B.ByteString -> G.Graph -> (Int, Int) -> [B.ByteString]
process dict board graph (n, d) = 
    [ word |
        path <- G.dfs d n graph,
        let word = pathToWord board path,
        T.contains word dict ]

然后在main中调用parMap rdeepseq (process dict board graph)

仅此一项就在我的系统上使用-N4 提供了 2 倍的整体加速,导致 i7 3770 上的运行时间为 0.65 秒,并且可以合理地从 1.2 秒(-N2)和 1.7 秒(在-N1.

对于更复杂的分析选项,您应该查看GHC Manual 和Real World Haskell。

我个人喜欢在像这样的小程序上使用“快速而肮脏”的分析方法:我为我的数据类型创建或派生NFData 实例,然后强制我的结构或部分程序使用return $!! something main 函数,然后使用+RTS -s 运行以进行诊断。因此,在您的原始程序构建中,trie 花费了 0.4 秒,生成路径花费了 0.6 秒,而且这两个任务都非常耗费 GC。

注意这里的 GC 负载其实是正常的。毕竟,您的 trie 有 Data.Map 节点,并且您在构建 trie 时会在那里进行大量插入,并且您还会在 Data.Set-s 的参与下生成 160 万条路径,以记录访问过的节点。

换句话说,GC 只是在做诚实的工作,而程序很慢,因为它有一个次优的算法。一种更快的方法是遍历字典中的所有单词并进行广度优先搜索以确定单词是否在 Boggle 表中。这是一个很有前途的策略,因为:

我们可以很快丢弃大部分单词(因为它们以不在表格中的字母开头)。

字数通常比可能的路径少得多。

不久前,我在 Haskell 中写了一个非常小的 Boggle program,它就是这样工作的。它比您在我的系统上的解决方案快大约 20-25 倍,并且目前没有任何并行化。

一种可能更快的方法是“正确”使用 trie,即。 e.在表上进行受约束的深度优先搜索,利用 trie 的结构修剪任何不可能产生有效单词的路径。我认为尽管在实践中很难让它比我的“愚蠢”方法更快,因为尝试实际大小的字典有绝对破坏 CPU 缓存的趋势。静态尝试虽然可以被惊人地压缩为有向无环词图,但这需要更多的工作。

【讨论】:

使用 BFS 方法的好主意;我简要地考虑了它,但是为了生成路径,并且由于它会给出相同的路径,我没有进一步考虑这个想法。我会试一试并报告。

以上是关于如何在 Haskell 中查找和修复由于 GC 导致的性能问题?的主要内容,如果未能解决你的问题,请参考以下文章

如何查找Haskell名称的包,版本和文档

如何在 Haskell 中查找程序的内存使用情况

国外数据和垃圾收集

如何在 Haskell 中定义树状 DAG

Haskell:从 ST / GC 泄漏内存不收集?

如何修复由 lambda 事件处理程序引起的 GC 周期?