【问题标题】:Why does putStrLn slow down with each iteration?为什么 putStrLn 每次迭代都会变慢?
【发布时间】:2020-11-12 18:05:47
【问题描述】:

我制作了一个小型生命游戏程序,它可以自行迭代几代人。问题是每次迭代时,putStrLn 函数都会大大减慢,我不知道为什么。代码如下:

import Control.Concurrent

data CellState = Dead | Alive

data Position = Position Integer Integer

type Generation = Position -> CellState

is_alive :: CellState -> Bool
is_alive Alive = True
is_alive Dead = False

neighbors :: Position -> [Position]
neighbors (Position x y) =
  [(Position (x-1) (y-1)), (Position x (y-1)),  (Position (x+1) (y-1)), (Position (x+1) y),
  (Position (x+1) (y+1)), (Position x (y+1)), (Position (x-1) (y+1)), (Position (x-1) y)]

alive_neighbors :: Generation -> Position -> Int
alive_neighbors generation position = length (filter is_alive (map generation (neighbors position)))

evolution :: Generation -> Generation
evolution generation position =
  case (alive_neighbors generation position) of
  2 -> if (is_alive (generation position)) then Alive else Dead
  3 -> Alive
  _ -> Dead

visualize_generation generation = map (visualize_line generation) [1..10]

visualize_line :: Generation -> Integer -> String
visualize_line generation y = concat (map (visualize_cell generation y) [1..10])

visualize_cell generation y x =
  case (generation (Position x y)) of
  Alive -> ['0']
  Dead -> ['.']

{-
bar (Position 1 2) = Alive
bar (Position 2 3) = Alive
bar (Position 3 3) = Alive
bar (Position 3 2) = Alive
bar (Position 3 1) = Alive
bar (Position x y) = Dead
-}

bar (Position 1 3) = Alive
bar (Position 2 3) = Alive
bar (Position 3 3) = Alive
bar (Position x y) = Dead

life :: Generation -> IO ()
life bar_ = do cls
               mapM_ putStrLn (visualize_generation bar_)
               threadDelay 1000000 
               life (evolution bar_)

cls = putStr "\ESC[2J"

我最初预计由于某种原因,每一代都会计算所有前几代,但似乎并非如此。如果是这种情况,我希望进化函数的计算时间会增加,而不是 putStrLn 函数打印缓慢。有什么想法可以让每一代的 putStrLn 函数都这么慢吗?

【问题讨论】:

  • 问题似乎在于您对Generation 的表示,以及当您多次应用evolution 时会发生什么。你能想象你实际上在内存中构建了什么样的结构吗?
  • 你说“我最初期望每一代由于某种原因也会计算所有前几代,但似乎并非如此。”。你是如何确定不是这种情况的? (我认为这种判断存在缺陷——实际上它多次重新计算前几代。)

标签: haskell conways-game-of-life


【解决方案1】:

(免责声明:这只是一个猜测,我可能错了。我没有进行实验来证实这一点。)

这是使用函数来表示网格所要付出的代价

type Generation = Position -> CellState

这是表示状态的一种优雅方式,但从长远来看效率不高。当你的算法运行时,它会创建很多闭包:

generation0 = \position -> ....
generation1 = \position -> .... use generation0
generation2 = \position -> .... use generation1
generation3 = \position -> .... use generation2
...

即使你只需要最后一代,所有上一代的数据仍然保存在内存中,因为它被上一代(间接)使用。因此,您永远不会释放内存,这已经很糟糕了。

更糟糕的是,每次使用 generation N,这将调用 generation N-1 多次 (8),然后又将调用 generation N-2 多次 (8),依此类推,直到生成 0 .这会导致指数级爆炸。

要解决此问题,您需要将数据表示更改为更有效的方式。我认为一些类似矩阵的类型可以工作。

【讨论】:

  • 记忆化可能是一种选择。记忆Integer-domain 函数会有点烦人,但并不可怕。这并不能解决永远的历史问题,但它应该解决重复计算问题。永远的历史问题需要考虑影响速度的极限。可能不太难,但有点挑剔。
猜你喜欢
  • 2021-10-14
  • 2011-10-11
  • 2011-08-16
  • 2015-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-28
相关资源
最近更新 更多