【问题标题】:How to keep very big elements on memory without exhausting the garbage collector?如何在不耗尽垃圾收集器的情况下将非常大的元素保留在内存中?
【发布时间】:2015-05-14 17:58:13
【问题描述】:

在 Haskell 中,我创建了一个包含 1000000 个 IntMap 的向量。然后,我使用 Gloss 以从该向量访问随机 intmap 的方式渲染图片。
也就是说,我已经将它们中的每一个都保存在内存中。渲染函数本身非常轻量级,所以性能应该不错。
然而,该程序以 4fps 运行。在分析后,我注意到 95% 的时间都花在了 GC 上。很公平:
GC 正在疯狂地扫描我的向量,即使它从未改变。

有什么方法可以告诉 GHC“这个大值是必需的并且不会改变 - 不要试图在其中收集任何东西”

编辑:下面的程序足以复制问题。

import qualified Data.IntMap as Map
import qualified Data.Vector as Vec
import Graphics.Gloss
import Graphics.Gloss.Interface.IO.Animate
import System.Random

main = do
    let size  = 10000000
    let gen i = Map.fromList $ zip [mod i 10..0] [0..mod i 10]
    let vec   = Vec.fromList $ map gen [0..size]
    let draw t = do 
            rnd <- randomIO :: IO Int
            let empty = Map.null $ vec Vec.! mod rnd size
            let rad   = if empty then 10 else 50
            return $ translate (20 * cos t) (20 * sin t) (circle rad)
    animateIO (InWindow "hi" (256,256) (1,1)) white draw

这会访问一个巨大矢量上的随机地图并绘制一个旋转圆,其半径取决于地图是否为空。
尽管这个逻辑非常简单,但程序在这里却以 1 FPS 左右的速度挣扎。

【问题讨论】:

  • 检查 GC 是否真的在收集内存,而不仅仅是浪费时间(应该有一些统计数据显示这一点),只是为了确保没有任何“更新”巨大的向量(即,只需稍作改动即可构建它的副本)。
  • 它是可变向量还是不可变向量?可变对象会大大降低 GC 性能。
  • 您对垃圾收集器所做工作的描述与我对 GHC 垃圾收集器工作原理的理解不相符。您对“GC 正在疯狂扫描我的向量,即使它从未改变”的说法有多大把握是正确的?
  • 其实我不太确定。另外,如果你愿意,我可以尝试制作一个最小的文件来复制问题。
  • 一些示例代码确实可以帮助我们帮助您。

标签: haskell optimization garbage-collection ghc


【解决方案1】:

光泽是这里的罪魁祸首。

首先,介绍一下 GHC 垃圾收集器的背景知识。 GHC(默认情况下)使用分代的、复制的垃圾收集器。这意味着堆由几个称为世代的内存区域组成。对象被分配到最年轻的一代。当一代变满时,会扫描活动对象并将活动对象复制到下一个较旧的代,然后将扫描的代标记为空。当最老一代已满时,活动对象会被复制到最老一代的新版本中。

一个重要的事实是 GC 只检查 live 对象。死物根本不会被触摸。这在收集大部分是垃圾的世代时非常有用,这在最年轻的一代中经常发生。长寿命的数据要经过多次 GC 是不好的,因为它会被重复复制。 (对于那些习惯于 malloc/free-style 内存管理的人来说,这也可能是违反直觉的,其中分配和释放都非常昂贵,但让对象长时间分配并没有直接成本。)

现在,“世代假设”是大多数对象要么是短命的,要么是长命的。长寿命的对象将很快在最老的一代中结束,因为它们在每个集合中都是活跃的。同时,大多数被分配的短期对象永远不会在最年轻的一代中存活;只有那些在收集时恰好还活着的人才会被提升到下一代。同样,那些确实得到提升的短命对象中的大多数也不会存活到第三代。因此,持有长寿命对象的最老一代应该会非常缓慢地填满,并且必须复制所有长寿命对象的昂贵集合应该很少发生。

现在,所有这些在您的程序中实际上都是正确的,除了一个问题:

    let displayFun backendRef = do
            -- extract the current time from the state
            timeS           <- animateSR `getsIORef` AN.stateAnimateTime

            -- call the user action to get the animation frame
            picture         <- frameOp (double2Float timeS)

            renderS         <- readIORef renderSR
            portS           <- viewStateViewPort <$> readIORef viewSR

            windowSize      <- getWindowDimensions backendRef

            -- render the frame
            displayPicture
                    windowSize
                    backColor
                    renderS
                    (viewPortScale portS)
                    (applyViewPortToPicture portS picture)

            -- perform GC every frame to try and avoid long pauses
            performGC

gloss 告诉 GC 每帧收集最老的一代!

如果这些集合预计花费的时间少于帧之间的延迟,这可能是一个好主意,但对于您的程序来说,这显然不是一个好主意。如果您从光泽中删除 performGC 调用,那么您的程序运行得非常快。据推测,如果你让它运行足够长的时间,那么最老的一代最终会被填满,当 GC 复制所有长期存在的数据时,你可能会延迟十分之几秒,但这比支付这笔费用要好得多每一帧。

话虽如此,有一张票#9052 是关于添加稳定代的,这也将很好地满足您的需求。有关详细信息,请参见此处。

【讨论】:

  • @Michael,这很奇怪,我没有看到任何版本使用超过大约 1.2G。
  • @ReidBarton 哇,我不能要求一个更好和更多信息的答案。你搞定了。谢谢!
  • 不用担心,我有时也会想。
【解决方案2】:

我会尝试编译 -with-rtsopts,然后使用堆 (-H) 和/或分配器 (-A) 选项。这些会极大地影响 GC 的工作方式。

更多信息在这里:https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/runtime-control.html

【讨论】:

  • 我想你的意思是-rtsopts-with-rtsopts 在编译时烘焙提供的选项。
【解决方案3】:

为了补充 Reid 的答案,我发现 performMinorGC(在 https://ghc.haskell.org/trac/ghc/ticket/8257 中添加)在这里是两全其美的。

在没有任何明确的 GC 调度的情况下,当 Nursery 耗尽时,我仍然会经常遇到与收集相关的丢帧。但是performGC 确实会在使用任何显着的长期内存使用时成为性能杀手。

performMinorGC 做我们想做的事,忽略长期记忆并按预期清理每一帧的垃圾 - 特别是如果您调整 -H-A 以确保每帧垃圾适合托儿所。

【讨论】:

    猜你喜欢
    • 2018-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-19
    • 2023-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多