Haskell数据向量,巨大的内存泄漏
我正在尝试用haskell和SDL1.2绑定制作一个基本的2D引擎(为了好玩,我只是在学习)。 理想情况下,世界是按程序生成的,逐块生成,允许自由探索 现在,我的区块由200*200个平铺组成,我使用以下类型表示:Haskell数据向量,巨大的内存泄漏,haskell,data-structures,memory-leaks,sdl,Haskell,Data Structures,Memory Leaks,Sdl,我正在尝试用haskell和SDL1.2绑定制作一个基本的2D引擎(为了好玩,我只是在学习)。 理想情况下,世界是按程序生成的,逐块生成,允许自由探索 现在,我的区块由200*200个平铺组成,我使用以下类型表示: Mat[Tile]=Vec.Vector(向量[Tile]) 这些功能包括: fromMat :: [[a]] -> Mat a fromMat xs = Vec.fromList [Vec.fromList xs' | xs' <- xs] (§) :: Mat a
Mat[Tile]=Vec.Vector(向量[Tile])
这些功能包括:
fromMat :: [[a]] -> Mat a
fromMat xs = Vec.fromList [Vec.fromList xs' | xs' <- xs]
(§) :: Mat a -> (Int, Int) -> a
v § (r, c) = (v Vec.! r) Vec.! c
fromMat::[[a]]->Mat a
fromMat xs=Vec.fromList[Vec.fromList xs'| xs'(Int,Int)->a
v§(r,c)=(v向量!r)向量!c
我使用循环列表的瓷砖,以允许精灵动画,后来的动态行为
在游戏循环的每一帧中,程序读取与当前摄像机位置相关的向量部分,显示相应的分片并返回一个新向量,其中每个循环列表都已被其尾部替换
以下是负责此操作的代码:
applyTileMat :: Chunk -> SDL.Surface -> SDL.Surface -> IO Chunk
applyTileMat ch src dest =
let m = chLand $! ch
(x,y) = chPos ch
wid = Vec.length (m Vec.! 0) - 1
hei = (Vec.length m) - 1
(canW,canH) = canvasSize ch in
do sequence $ [ applyTile (head (m § (i,j))) (32*(j-x), 32*(i-y)) src dest | i <- [y..(y+canH)], j <- [x..(x+canW)]]
m' <-sequence $ [sequence [(return $! tail (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]] --weird :P
return ch { chLand = fromMat m' }
applyTileMat::Chunk->SDL.Surface->SDL.Surface->IO Chunk
APPLYLEMAT ch src dest=
设m=chLand$!ch
(x,y)=chPos-ch
宽度=向量长度(m向量!0)-1
hei=(向量长度m)-1
(canW,canH)=画布尺寸
执行序列$[applyTile(head(m§(i,j))(32*(j-x),32*(i-y))src dest | i问题确实在于向量在元素中是惰性的。首先,让我们看看为什么您的示例不起作用
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
fromMat
是下一个罪魁祸首。fromMat
不强制内部向量,也不强制元素。因此,对旧向量的引用会无限期地停留在thunk中
通常正确的解决方案是导入Control.DeepSeq
,并使用force
或$!!
来完全计算向量。不幸的是,由于循环列表(尝试强制一个会导致无限循环),我们无法在这里这样做
我们真正需要的是一个函数,它将向量的所有元素都转换为弱头范式:
whnfElements :: Vector a -> Vector a
whnfElements v = V.foldl' (flip seq) () v `seq` v
我们可以使用它为向量定义一个严格的映射:
vmap' :: (a -> b) -> Vector a -> Vector b
vmap' f = whnfElements . V.map f
现在更新变成:
update :: Mat [Tile] -> Mat [Tile]
update = (vmap' . vmap') tail
在黑暗中拍摄,但你是否对Data.Vector.unbox进行了同样的测试?@hasufell OP在向量中存储[Tile]
-s,这是无法取消绑定的。是的,我考虑过,但正如András所说,我似乎只能存储带有未绑定向量的基本类型。提示:使用向量[Tile]
,而不是向量(Vector)[瓷砖])
。你可以把它当作2D来索引。你也可以使用Vec.map
和Vec.mapM
进行更新,而不是索引,Vec.fromList
和sequence
。太棒了,成功了!但是如果我只需要查看当前向量的一小部分来构建下一个向量,会发生什么呢?必要吗减少所有元素到WHNF的能力迫使我检查整个向量?如果你使用向量,那么每次更新都必须复制整个向量。因此WHNF跳过所有元素不会增加太多开销,也不能跳过,因为跳过任何单个元素都会导致内存泄漏。如果你想提高效率日期,您必须使用矢量以外的内容。Data.IntMap
或Data.HashMap
可能是最快的,但请注意它们有相当大的时空开销。也许您可以尝试使用包含一定大小(16-32)的矢量块的Map
;这样可以获得更好的空间效率。每次更新时必须至少复制一个完整的块,但这是一个有界且可能可以容忍的量,与向量不同。这次我定义了函数vImap'f=whnfElements.Vec.imap f
和updatem(I,j,v)=vImap'(\k v'->如果k==i那么vImap'(\l v'->如果l==j那么v else v')v else v')m
以允许基于索引的更新。使用它会导致内存泄漏,即使我在vImap的定义中使用了@AndráswhnfElements
。我看不出这次thunks在哪里累积。看看else v'
部分。你没有在那里做whnfElement
。
vmap' :: (a -> b) -> Vector a -> Vector b
vmap' f = whnfElements . V.map f
update :: Mat [Tile] -> Mat [Tile]
update = (vmap' . vmap') tail