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

时间:2015-05-14 17:58:14

标签: haskell optimization garbage-collection ghc

在Haskell中,我创建了一个1000000 IntMaps的Vector。然后我使用Gloss以一种方式渲染图片,从该向量访问随机的intmaps 也就是说,我把每一个都留在了记忆中。渲染功能本身非常轻巧,因此性能应该很好 然而,该计划以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。

3 个答案:

答案 0 :(得分:4)

光泽是这里的罪魁祸首。

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

一个重要的事实是,GC只会检查 live 对象。永远不会触及死亡物体。收集几乎是垃圾的世代时,这是很好的,这种情况经常发生在最年轻的一代。如果长期数据经历许多GC,那将是不好的,因为它将被重复复制。 (对于那些习惯于malloc / free-style内存管理的人来说,这也是违反直觉的,其中分配和释放都非常昂贵,但是长时间分配对象没有直接成本。)

现在,&#34;世代假设&#34;是大多数物体要么是短寿命的,要么是长寿命的。长寿命的物体很快就会在最古老的一代中结束,因为它们在每个系列中都活着。与此同时,分配的大部分短命物体永远不会存在于最年轻的一代;只有那些在收集时才会活着的人才会被提升到下一代。同样,大多数获得晋升的短期物品都无法生存到第三代。因此,持有长寿命对象的最老一代应该填充得非常慢,而且必须复制所有长寿命对象的昂贵集合应该很少发生。

现在,除了一个问题外,所有这一切在您的程序中都是如此:

    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,这也很适合您的需求。有关详细信息,请参阅此处。

答案 1 :(得分:2)

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

此处有更多信息:https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/runtime-control.html

答案 2 :(得分:1)

要添加到Reid的答案,我发现performMinorGC(在https://ghc.haskell.org/trac/ghc/ticket/8257中添加)是这两个世界中最好的。

在没有任何明确的GC调度的情况下,当托儿所耗尽时,我仍然经常收集与收集相关的帧丢失。但是,performGC一旦存在任何重要的长期内存使用,确实会导致性能下降。

performMinorGC执行我们想要的操作,忽略长期记忆并可预测地清理每个帧中的垃圾 - 特别是如果您调整-H-A以确保每帧垃圾适合托儿所。