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

Jus*_*ond 114 parallel-processing haskell raytracing

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

在我的第一次尝试中,我使用parBuffer了缓冲区大小为50 的并行策略.我之所以选择parBuffer它是因为它只是在消耗火花的情况下遍历列表,并且不会强制列表的主干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强制列表的主干.

然后我试着编写自己的策略,懒洋洋地将列表分成块,然后传递块parBuffer并连接结果.

 concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))
Run Code Online (Sandbox Code Playgroud)

这运行时间为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)))
Run Code Online (Sandbox Code Playgroud)

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

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
Run Code Online (Sandbox Code Playgroud)

Thi*_*eye 2

不是问题的解决方案,而是原因的提示:

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

新数据被分配在512kb“苗圃”中。一旦耗尽,就会发生“minor GC”——它会扫描 Nursery 并释放未使用的值。

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