【问题标题】:Space leaks with Haskell's cereal library?Haskell 的谷类库空间泄漏?
【发布时间】:2013-12-31 17:40:43
【问题描述】:

作为一个名为“beercan”的业余项目,我正在对 Torchlight 游戏的资源文件进行逆向工程。使用正常的十六进制编辑器,我尝试猜测文件的结构,然后对我的想法进行建模,使用cereal 编写Getters(以及后来的一些Putters),并尝试@987654330 @ 库应用程序中的每个文件。

我刚刚开始处理 Torchlight 的编译布局文件(TL1 中的*.LAYOUT,TL2 中的*.LAYOUT.cmp)。结果证明格式比 dat 文件有点棘手,但我想我想出了basic structurehow they are encoded in the TL2 files。所以我正在尝试制作文件版本、标签号和猜测的数据类型的映射。

为此,我编写了an application 来扁平化数据结构,只留下叶子值的猜测类型,每个都用文件版本以及节点和叶子标记号进行注释。我将其转换为从文件版本和标签号到一组猜测类型的映射。 对于每个文件,我希望这张地图在内存中占用的文件大小可能是文件大小的两倍。(但不确定。)然后,我合并这些地图,然后打印地图。

出于某种原因,即使我只占用 20MB 的文件(100 个文件),内存使用量也会线性增加至大约 200MB,然后减少到生成地图的最终大小,然后在我打印时迅速缩小。

我不希望这样的内存使用。有谁知道我该如何解决?我尝试在解码后强制值(使用 deepseq),我尝试在数据类型中添加 bang,但这并没有真正帮助。我尝试复制我保留在文件结构中的所有字节串,这稍微降低了内存使用量,但它仍然高得令人无法接受,尤其是当我想分析整个数据集(200MB 以上的原始文件)时。


-edit- 我推送了一个(不是很 S)SCCE 来演示性能问题,(不小心)以及我的分析结果。

  1. 克隆存储库。
  2. cabal configure,带有启用分析的标志(需要--enable-library-profiling --enable-executable-profiling --ghc-options="-rtsopts -prof"是否正常?)
  3. cabal build
  4. cd test,然后运行 ​​StressTest.sh

此脚本尝试加载常规 TL2 布局文件 100 次。在我的机器上,top 说它需要大约 500MB 的内存,并且 profiling 结果与我上面的描述一致。

【问题讨论】:

  • 你能发一个SSCCE吗?我们可以运行一段代码,它可以演示内存问题,而无需探索整个应用程序。也许只是一个读取/dev/zero 多次的虚拟解析器或类似的东西。
  • 如果有帮助,我提供了一个压力测试用例来演示问题,但如果不发明自己的文件格式,我真的不知道如何解决问题:/

标签: haskell memory-leaks unmarshalling


【解决方案1】:

我完全同意@petrpudlak,对于“为什么我的代码使用这么多内存?”这个问题,我们需要实际的代码来制作任何有意义的 cmets。 :)(抱歉,您确实提供了代码),但是,您描述的一些模式在 Haskell 中非常典型,可以进行一些通用讨论。

首先,请注意,原生 Haskell 类型使用的内存比您想象的要多得多。查看http://www.haskell.org/haskellwiki/GHC/Memory_Footprint 的 ghc 内存占用页面。请注意,即使是简单的 Char 也会占用 16 个字节的内存!添加到字符串中的链表项的指针,您将轻松使用比您可能猜到的多一个数量级的内存。如果内存很重要,您应该使用另一种数据类型,如 Data.Text 或 Data.ByteString,它们在内部存储字符串更像 c 将(作为内存中的一个字节块,每个字符 1-4 个字节,具体取决于编码和使用什么字符)。如果字符串以外的数据有问题,您可以将未装箱的数组用于任意数据类型。

其次,如果可能,您可以通过串行处理项目来减少内存使用量(内存将立即被垃圾回收)。 Haskell 懒惰通常会自动为您执行此操作,例如,尝试运行以下程序

import Data.Char
main = interact $ map toUpper

当您键入时,输出将连续出现(您的操作系统,而不是 Haskell,可能会缓冲整行,因此您可能需要在看到任何内容之前点击“输入”,但您会看到每个“输入”的输出更新)。不是将整个输入加载到内存中然后一次处理所有内容,而是创建 Char 内存并按 Char 对 Char 进行垃圾收集。

当然,这并不总是可能的(即,如果您必须以非常非本地的方式处理数据),但大多数情况下,至少部分代码可以通过这种方式重构以减少总内存使用量.


编辑-抱歉,我刚刚意识到您确实发布了代码链接,并且您正在使用 ByteString .....所以我写的一些内容无效。但我仍然看到盒装列表和 ByteString 的解包,所以我将保持原样。

【讨论】:

  • 通过串行处理所有内容(通过编写M.unionsWith 对映射IO 的累积进行排序,并且对累加器有严格要求)解决了内存使用问题。我可以将使用量减少到大约 2MB。谢谢,我想我可以从这里改进它! :D
【解决方案2】:

内存使用模式听起来像是您的应用程序正在构建大量不必要的 thunk,然后在评估这些 thunk 时内存消耗开始下降。我只是快速浏览了您的代码,但您可以尝试的一个简单更改是用Data.Map.Strict 替换所有Data.Map 的导入。如果您要对 Map 中的值进行大量更新而不强制在两者之间进行评估,这一点尤其重要。

您应该注意的另一件事是 replicateM 在严格的 monad 中使用较大的数字时效率非常低(参见例如 this answer)。我不确定您通常在应用程序中处理哪些类型的计数,但请记住这一点。

在像 LeafValue 类型这样的简单容器数据类型中使用严格字段并使用 -funbox-strict-fields(当然还有 -O2)进行编译也可能会有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-13
    • 1970-01-01
    • 2015-02-27
    • 2017-12-03
    相关资源
    最近更新 更多