如何在不耗尽垃圾收集器的情况下将非常大的元素保留在内存中?

How to keep very big elements on memory without exhausting the garbage collector?

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

有没有办法告诉GHC "this big value is needed and will not change - do not try to collect anything inside it".

编辑:下面的程序足以重现该问题。

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。

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

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

光泽是这里的罪魁祸首。

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

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

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

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

    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 每一帧收集最旧的一代!

如果无论如何预计这些集合花费的时间少于帧之间的延迟,这可能是个好主意,但对于您的程序来说这显然不是一个好主意。如果您从 gloss 中删除 performGC 调用,那么您的程序 运行 会很快。大概如果你让它 运行 足够长的时间,那么最老的一代最终会填满,你可能会延迟零点几秒,因为 GC 会复制所有长期存在的数据,但这要好得多而不是每帧支付该费用。

综上所述,关于添加一个稳定的生成有一个问题 #9052,这也能很好地满足您的需求。有关更多详细信息,请参阅此处。

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

在没有任何明确的 GC 调度的情况下,随着 nursery 变得耗尽,我仍然会频繁出现与收集相关的帧丢失。但是 performGC 一旦有任何显着的长期内存使用,确实会导致性能下降。

performMinorGC 做我们想要的,忽略长期记忆并可预见地清理每一帧的垃圾——尤其是当你调整 -H-A 以确保每个帧-frame 垃圾适合托儿所。