强制Haskell的Data.Binary.Get进行顺序处理

3

尝试使用 language-java-classfile 导入基本的 Java 运行库 rt.jar 后,发现它会占用大量内存。

我已将演示问题的程序缩减到 100 行,并上传到hpaste。如果不在第 94 行之前强制评估 stream,我就没有机会运行它,因为它会耗尽我的所有内存。在传递给 getClass 之前强制执行 stream 可以完成,但仍会占用大量内存。

  34,302,587,664 bytes allocated in the heap
  32,583,990,728 bytes copied during GC
     139,810,024 bytes maximum residency (398 sample(s))
      29,142,240 bytes maximum slop
             281 MB total memory in use (4 MB lost due to fragmentation)

  Generation 0: 64992 collections,     0 parallel, 38.07s, 37.94s elapsed
  Generation 1:   398 collections,     0 parallel, 25.87s, 27.78s elapsed

  INIT  time    0.01s  (  0.00s elapsed)
  MUT   time   37.22s  ( 36.85s elapsed)
  GC    time   63.94s  ( 65.72s elapsed)
  RP    time    0.00s  (  0.00s elapsed)
  PROF  time   13.00s  ( 13.18s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time  114.17s  (115.76s elapsed)

  %GC time      56.0%  (56.8% elapsed)

  Alloc rate    921,369,531 bytes per MUT second

  Productivity  32.6% of total user, 32.2% of total elapsed

我以为问题在于ConstTable一直存在,所以我也尝试在第94行强制使用cls。但这只会使内存消耗和运行时间更糟:

  34,300,700,520 bytes allocated in the heap
  23,579,794,624 bytes copied during GC
     487,798,904 bytes maximum residency (423 sample(s))
      36,312,104 bytes maximum slop
             554 MB total memory in use (10 MB lost due to fragmentation)

  Generation 0: 64983 collections,     0 parallel, 71.19s, 71.48s elapsed
  Generation 1:   423 collections,     0 parallel, 344.74s, 353.01s elapsed

  INIT  time    0.01s  (  0.00s elapsed)
  MUT   time   40.60s  ( 42.38s elapsed)
  GC    time  415.93s  (424.49s elapsed)
  RP    time    0.00s  (  0.00s elapsed)
  PROF  time   56.53s  ( 57.71s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time  513.07s  (524.58s elapsed)

  %GC time      81.1%  (80.9% elapsed)

  Alloc rate    844,636,801 bytes per MUT second

  Productivity   7.9% of total user, 7.7% of total elapsed

我的问题基本上是,如何强制顺序处理涉及的文件,以便在处理完每个文件后,只有字符串结果 (cls) 保留在内存中?


你可以进行一些堆分析,并将图表发布在你的问题中,这样怎么样? - Thomas M. DuBuisson
我已经将两个版本的堆分析输出添加到了 hpaste 上。 - Cactus
你尝试过使用“cereal”软件包吗?只是一个想法,但如果你知道文件足够小的话,严格的字节串通常会有所帮助。 - Thomas M. DuBuisson
我在使用 cereal 时遇到了两个问题:一是它似乎不支持按固定字节顺序的浮点数,另一个问题是缺少 MonadFix 实例。 - Cactus
3个回答

2
编辑2:我刚意识到你的代码实现了这个功能:
stream <- BL.pack <$> fileContents [] classfile

不要这么做。 pack 函数非常慢。你需要找到一个不涉及使用 pack 创建 ByteString 的解决方案。

我保留下面的回答,因为我认为它仍然适用,但这几乎肯定是最大的问题。

很遗憾,我无法测试这个,因为我不认识您所有的导入。

如果你只想让结果 cls 保留在内存中,为什么不强制它而不是强制 stream?将第94行更改为:

cls `seq` return cls

可能需要使用deepseq而不仅仅是seq,尽管我怀疑在这里仅使用seq就足够了。

然而,我认为有一个更好的解决方案,那就是使用mapM_代替mapM。通常更好的风格(几乎总是更好的性能)是创建一个执行每个结果所需操作的函数,而不是返回一个列表。在这里,可以将主函数更改为:

main = do 
  withArchive [CheckConsFlag] jarPath $ do
    classfiles <- filter isClassfile <$> fileNames []
    forM_ classfiles $ \classfile -> do 
      stream <- BL.pack <$> fileContents [] classfile
      let cls = runGet getClass stream
      lift $ print cls

现在print被提升到每个classfile传递给forM_的函数中。变量cls内部使用,不会返回值,因此在每次forM_迭代中都完全求值并快速GC。

在更大的应用程序中使用这种风格可能需要进行一些重构甚至重新设计,但结果可能是值得的。

编辑:如果你要费力重新设计代码,可以使用iteratees来完全避免这个问题。


但在实际的程序中,我的结果将是cls列表。另外,第二个输出来自流seq cls seq返回cls(如果我表达不清楚,请原谅)。 - Cactus
@Cactus,抱歉,我刚刚在我的答案顶部添加了一条注释,请查看。 - John L
此外,我知道你的结果是cls列表,但我认为最好创建一个函数来处理每个cls,而不是创建它们的列表。但我认为这不是最重要的问题。 - John L
我使用的模块来自Binary、data-binary-ieee754、bytestring、containers、missingh和libzip(希望我没有漏掉任何一个)。 - Cactus
1
我应该使用什么替代BL.pack?使用BS.pack然后再用BL.fromChunks会有帮助吗? - Cactus
显示剩余7条评论

1

你在第94行强制评估cls的想法是正确的。但我猜你的方法并不成功。请参考paste查看我的版本,它只需要大约40MB的运行空间,而不是220MB。

关键是要强制将cls规约为正常形式,这可以通过rnf cls来完成。而且这必须在调用return之前发生:

rnf cls `seq` return cls

或者,你可以使用Control.Exception.evaluate:

evaluate $ rnf cls return cls


0

感谢您的建议。

我认为对于我的具体问题,解决方案将是分批处理.jar文件 - 幸运的是,内部类始终在与其外部类相同的目录中,因此无需在一次运行中处理所有50兆字节。

唯一我不太明白的是是否可以通过枚举器使用libzip,还是需要一个新的libzip实现?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接