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




raytracing (2)

我试图平行化射线追踪器。 这意味着我有一个很长的小计算列表。 该香草程序运行在67.98秒的特定场景和13 MB的总内存使用率和99.2%的生产率。

在我的第一次尝试中,我使用了缓冲区大小为50的并行策略parBuffer 。我选择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强制列表的脊椎。

然后,我试图编写自己的策略,将列表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,但没有看到任何区别。

编辑2: 这是一个链接到github回购专用于这个问题 。 我已将分析结果包含在分析文件夹中。

编辑3:这是代码的相关位:

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

Answers

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

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

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

因此,如果将数据切分为更小的块,则可以使引擎尽早进行清理 - GC开始执行。


一个优雅的方法是使用部分功能。

如果你知道你希望foo的第一个参数是myArg,你可以创建一个新的函数栏

from functools import partial
bar = partial(foo, myARg)

bar(otherArg)然后将返回foo(myArg,otherArg)





haskell parallel-processing raytracing