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

时间:2015-07-26 20:13:53

标签: haskell parallel-processing 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))

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

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

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

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

EDIT2: Here is a link to a github repo dedicated to this question。我已将分析结果包含在分析文件夹中。

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

1 个答案:

答案 0 :(得分:2)

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

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

  

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

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