Haskell线程堆溢出尽管总内存使用量只有22Mb?



parallel-processing raytracing (1)

我正在尝试并行化光线跟踪器。 这意味着我有一个很长的小计算列表。 vanilla程序在67.98秒内在特定场景上运行,总内存使用量为13 MB,生产率为99.2%。

在我的第一次尝试中,我使用了并行策略 parBuffer ,缓冲区大小为50.我选择了 parBuffer 因为它只能像消耗火花一样快地遍历列表,并且不会像 parList 一样 parList 列表的 parList ,这将使用由于列表很长,因此内存很多。 使用 -N2 ,它运行时间为100.46秒,总内存使用量为14 MB,生产率为97.8%。 火花信息是: SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)

大部分失败的火花表明火花的粒度太小,所以接下来我尝试使用策略 parListChunk ,它将列表分成块并为每个块创建一个火花。 我得到了最好的结果,块大小为 0.25 * imageWidth 。 该程序运行时间为93.43秒,总内存使用量为236 MB,生产率为97.3%。 火花信息是: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled) 。 我相信更大的内存使用是因为 parListChunk 强制列表的 parListChunk

然后我尝试编写自己的策略,将列表 parBuffer 分成块,然后将块传递给 parBuffer 并连接结果。

 concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))

这运行时间为95.99秒,总内存使用量为22MB,生产率为98.8%。 在所有火花都被转换并且内存使用率低得多的意义上,这是成功的,但速度没有提高。 以下是事件日志配置文件的一部分图像。

正如您所看到的,由于堆溢出,线程正在停止。 我尝试添加 +RTS -M1G ,它将默认堆大小一直增加到1Gb。 结果没有改变。 我读到如果堆栈溢出,Haskell主线程将使用堆中的内存,所以我也尝试使用 +RTS -M1G -K1G 增加默认堆栈大小,但这也没有影响。

还有什么我可以尝试的吗? 如果需要,我可以发布更详细的内存使用情况或事件日志的分析信息,我没有全部包含它,因为它是很多信息,我不认为所有这些都是必要的。

编辑:我正在阅读有关 Haskell RTS多核支持的内容 ,并且它讨论了每个内核都有一个HEC(Haskell执行上下文)。 除了别的以外,每个HEC都包含一个分配区域(它是单个共享堆的一部分)。 每当HEC的分配区域耗尽时,必须执行垃圾收集。 似乎是一个控制它的 RTS选项 ,-A。 我试过-A32M,但没有看到任何区别。

EDIT2: 这是一个专门针对这个问题的github仓库的链接 。 我已将分析结果包含在分析文件夹中。

EDIT3:这是相关的代码:

render :: [([(Float,Float)],[(Float,Float)])] -> World -> [Color]
render grids world = cs where 
  ps = [ (i,j) | j <- reverse [0..wImgHt world - 1] , i <- [0..wImgWd world - 1] ]
  cs = map (colorPixel world) (zip ps grids)
  --cs = withStrategy (parListChunk (round (wImgWd world)) rdeepseq) (map (colorPixel world) (zip ps grids))
  --cs = withStrategy (parBuffer 16 rdeepseq) (map (colorPixel world) (zip ps grids))
  --cs = concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map (colorPixel world) (zip ps grids)))

网格是随机浮点数,由colorPixel预先计算并使用.colorPixel的类型为:

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color

不是你问题的解决方案,而是对原因的暗示:

Haskell似乎在内存重用方面非常保守,当解释器看到回收内存块的可能性时,它就是这样做的。 您的问题描述符合此处描述的次要GC行为(底部) https://wiki.haskell.org/GHC/Memory_Management

新数据分配在512kb“托儿所”中。 一旦耗尽,就会发生“次要GC” - 它会扫描托儿所并释放未使用的值。

因此,如果您将数据分成较小的块,则可以让引擎更早地进行清理 - GC启动。





raytracing