哈斯克尔的谷物库空间泄漏?



作为一个叫做'beercan'的爱好项目,我正在对火炬之光游戏的资源文件进行逆向工程。使用一个不错的十六进制编辑器,我尝试猜测文件的结构,然后我建模我的想法,使用cereal编写Get之三(后来是一些Put之三),并尝试decode库应用程序中的每个文件。

我刚刚开始使用 Torchlight 编译的布局文件(TL1 中*.LAYOUT,TL2*.LAYOUT.cmp)。事实证明,该格式比 dat 文件有点棘手,但我想我弄清楚了基本结构,以及它们如何在 TL2 文件中编码。所以我正在尝试制作文件版本、标签号和猜测数据类型的地图。

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

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

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


-编辑- 我已经推送了一个(不是很 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 的内存,分析结果与我上面的描述一致。

我完全同意@petrpudlak,我们需要实际的代码来对"为什么我的代码使用这么多内存?"这个问题做出任何有意义的评论:)(对不起,你确实提供了代码),但是,你描述的一些模式在Haskell中非常典型,一些通用的讨论是可能的。

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

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

import Data.Char
main = interact $ map toUpper

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

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


编辑 - 对不起,我刚刚意识到您确实发布了代码链接,并且您正在使用字节字符串..... 所以我写的一些内容是无效的。 但我仍然看到盒装列表和解压缩 ByteString,所以我将保持答案不变。

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

您应该注意的另一件事是,在严格的 monad 中,replicateM对于较大的数字,效率非常低(参见例如此答案)。我不确定您通常在应用程序中处理哪种计数,但最好记住这一点。

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

最新更新