在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。
答案 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
以确保每帧垃圾适合托儿所。