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

Mai*_*tor 15 optimization garbage-collection haskell ghc

在Haskell中,我创建了一个1000000 IntMaps的Vector.然后我使用Gloss以一种访问该向量的随机位图的方式呈现图片.
也就是说,我把每一个都留在了记忆中.渲染函数本身非常轻量级,因此性能应该很好.
然而,该计划以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
Run Code Online (Sandbox Code Playgroud)

这将访问一个巨大的矢量上的随机地图并绘制一个旋转圆,其半径取决于地图是否为空.
尽管这个逻辑非常简单,但该程序在这里大约只有1 FPS.

Rei*_*ton 4

光泽是这里的罪魁祸首。

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

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

现在,“世代假设”认为大多数物体要么是短命的,要么是长命的。寿命长的对象很快就会出现在最老的一代中,因为它们在每次收集时都处于活动状态。同时,大多数分配的短期对象永远不会在最年轻的一代中存活下来;只有那些在收集时恰好活着的才会被提升到下一代。同样,大多数那些确实得到提升的短命对象将无法生存到第三代。因此,保存长寿命对象的最老一代应该非常缓慢地填充,并且必须复制所有长寿命对象的昂贵集合应该很少发生。

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

    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
Run Code Online (Sandbox Code Playgroud)

光泽度告诉 GC 每帧收集最老的一代!

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

话虽如此,有一张关于添加稳定一代的票#9052,这也很适合您的需求。请参阅此处了解更多详细信息。

  • 不用担心,我有时也会想。 (2认同)