Haskell Data.Vector,巨大的内存泄漏
Haskell Data.Vector, huge memory leak
我正在尝试使用 haskell 和 SDL1.2 绑定制作一个基本的 2D 引擎(为了好玩,我只是在学习)。
理想情况下,世界是程序生成的,一块一块地,允许自由探索。
现在我的块由 200*200 个图块组成,我使用以下类型表示:
Mat [Tile] = Vec.Vector (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
我正在使用图块的循环列表以允许精灵动画,以及稍后的动态行为。
游戏循环的每一帧,程序读取与当前摄像机位置相关的向量部分,显示相应的图块和return一个新向量,其中每个循环列表都已被替换靠它的尾巴。
这是负责此操作的代码:
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' }
第一个序列做显示部分,第二个return是新向量m'。
起初我使用下面的理解来得到 m'
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
但这样做会导致内存使用量不断增加。我认为这与延迟评估有关,阻止了数据被正确地垃圾收集,但我真的不明白为什么。
在这种特殊情况下,它并不重要,因为我必须查看整个矢量。但是我不知道如果我只想 "update" 每帧我的块的一部分,我该怎么办,从而制作一个新的块,只包含前一个块的部分数据。
我可能没有按照预期的方式使用 Data.Vector,但它是我在 O(n) 随机访问中发现的最简单的数据结构。
完整代码在那里:
https://github.com/eniac314/wizzard/blob/master/tiler.hs
问题确实是向量在元素中是惰性的。首先,让我们看看为什么您的示例不起作用。
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
!m
中的刘海图案作用不大。 !
所做的只是确保变量是构造函数或 lambda,而不是函数应用程序。这里 !m
可以被识别为 []
或 (:)
而无需评估任何元素。同样,įd $!
-s 不强制内部列表的任何实际元素。
return ch { chLand = fromMat m' }
fromMat
是下一个罪魁祸首。 fromMat
不强制内部向量,也不强制元素。因此,对旧向量的引用会无限期地保留在 thunk 中。
通常正确的解决方案是导入 Control.DeepSeq
,并使用 force
或 $!!
来完全评估向量。不幸的是,由于循环列表,我们不能在这里这样做(试图强制一个结果导致无限循环)。
我们真正需要的是一个将向量的所有元素转化为弱头部范式的函数:
whnfElements :: Vector a -> Vector a
whnfElements v = V.foldl' (flip seq) () v `seq` v
我们可以使用它来为向量定义一个严格的map
:
vmap' :: (a -> b) -> Vector a -> Vector b
vmap' f = whnfElements . V.map f
现在更新为:
update :: Mat [Tile] -> Mat [Tile]
update = (vmap' . vmap') tail
我正在尝试使用 haskell 和 SDL1.2 绑定制作一个基本的 2D 引擎(为了好玩,我只是在学习)。 理想情况下,世界是程序生成的,一块一块地,允许自由探索。
现在我的块由 200*200 个图块组成,我使用以下类型表示:
Mat [Tile] = Vec.Vector (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
我正在使用图块的循环列表以允许精灵动画,以及稍后的动态行为。
游戏循环的每一帧,程序读取与当前摄像机位置相关的向量部分,显示相应的图块和return一个新向量,其中每个循环列表都已被替换靠它的尾巴。
这是负责此操作的代码:
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' }
第一个序列做显示部分,第二个return是新向量m'。
起初我使用下面的理解来得到 m'
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
但这样做会导致内存使用量不断增加。我认为这与延迟评估有关,阻止了数据被正确地垃圾收集,但我真的不明白为什么。
在这种特殊情况下,它并不重要,因为我必须查看整个矢量。但是我不知道如果我只想 "update" 每帧我的块的一部分,我该怎么办,从而制作一个新的块,只包含前一个块的部分数据。
我可能没有按照预期的方式使用 Data.Vector,但它是我在 O(n) 随机访问中发现的最简单的数据结构。
完整代码在那里: https://github.com/eniac314/wizzard/blob/master/tiler.hs
问题确实是向量在元素中是惰性的。首先,让我们看看为什么您的示例不起作用。
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
!m
中的刘海图案作用不大。 !
所做的只是确保变量是构造函数或 lambda,而不是函数应用程序。这里 !m
可以被识别为 []
或 (:)
而无需评估任何元素。同样,įd $!
-s 不强制内部列表的任何实际元素。
return ch { chLand = fromMat m' }
fromMat
是下一个罪魁祸首。 fromMat
不强制内部向量,也不强制元素。因此,对旧向量的引用会无限期地保留在 thunk 中。
通常正确的解决方案是导入 Control.DeepSeq
,并使用 force
或 $!!
来完全评估向量。不幸的是,由于循环列表,我们不能在这里这样做(试图强制一个结果导致无限循环)。
我们真正需要的是一个将向量的所有元素转化为弱头部范式的函数:
whnfElements :: Vector a -> Vector a
whnfElements v = V.foldl' (flip seq) () v `seq` v
我们可以使用它来为向量定义一个严格的map
:
vmap' :: (a -> b) -> Vector a -> Vector b
vmap' f = whnfElements . V.map f
现在更新为:
update :: Mat [Tile] -> Mat [Tile]
update = (vmap' . vmap') tail