Haskell数据向量,巨大的内存泄漏

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

我正在尝试用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 -> (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ás
whnfElements
。我看不出这次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