优化康威的“生命游戏”

时间:2008-09-02 20:15:14

标签: algorithm performance language-agnostic conways-game-of-life

为了实验,我(很久以前)实施了Conway的Game of Life(我知道this相关的问题!)。

我的实现通过保留2个布尔数组来表示“最后状态”和“正在更新状态”(每次迭代时交换2个数组)。虽然速度相当快,但我常常想知道如何优化它。

例如,一个想法是在迭代N处预先计算可以在迭代(N + 1)处修改的区域(这样如果一个单元不属于这样的区域,它甚至不会被考虑在迭代(N + 1)处进行修改。我知道这很模糊,我从来没有花时间详细介绍......

您对如何优化(速度)Game of Life迭代有任何想法(或经验!)吗?

12 个答案:

答案 0 :(得分:31)

我将从另一个问题引用我的答案,因为我提到的章节有一些非常有趣和精细的解决方案。一些实现细节是c和/或汇编,是的,但大多数情况下算法可以使用任何语言:

  

章节1718   Michael Abrash的Graphics Programmer's Black Book是其中之一   我有史以来最有趣的读物   了。这是思考的一课   箱子外面。整本书是   非常棒,但最终优化了   生命游戏的解决方案是   令人难以置信的编程。

答案 1 :(得分:14)

有一些超快速实现(从内存中)将8个或更多相邻方块的单元格表示为位模式,并将其用作大量预先计算值的索引,以在单个机器指令中确定单元格是否为活着还是死了。

点击这里:

http://dotat.at/prog/life/life.html

还有XLife:

http://linux.maruhn.com/sec/xlife.html

答案 2 :(得分:10)

你应该研究Hashlife,最终的优化。它使用了skinp提到的quadtree方法。

答案 3 :(得分:2)

算法本身具有固有的可并行性。在未经优化的CUDA内核中使用相同的双缓冲方法,我在4096x4096包裹的世界中每代大约需要25ms。

答案 4 :(得分:2)

什么是最有效的算法主要取决于初始状态。

如果大多数单元格已经死亡,您可以通过跳过空白部分而不是逐个单元地计算内容来节省大量CPU时间。

我认为,首先检查完全死亡的空间是有意义的,当你的初始状态是“随机的,但有可能生命低于5%。”

我只是将矩阵分成两半,然后开始检查较大的矩阵。

所以如果你有10,000 * 10,000的字段,你首先要累积左上角的5,000 * 5,000的状态。

如果第一季度的状态总和为零,您现在可以完全忽略第一季度,然后检查右上角的5,000 * 5,000。

如果其状态总和> 0,您现在将第二季度再分为4个部分 - 并重复检查每个子空间的生命周期。

你现在可以下到8 * 8或10 * 10的子帧(不知道这里最有意义的是什么)。

无论何时找到生命,都要将这些子空间标记为“有生命”。

只有“有生命”的空间需要划分为更小的子空间 - 可以跳过空的空格。

当您完成为所有可能的子空间分配“has life”属性时,您最终得到一个子空间列表,您现在只需向每个方向延伸+1 - 使用空单元格 - 并执行常规(或修改)生命游戏规则给他们。

你可能认为将10,000 * 10,000个spae分成8 * 8的子空间是很多os任务 - 但是积累它们的状态值实际上比执行每个单元格的GoL算法加上它们的计算工作要少得多。 8个邻居加上比较数字和存储净迭代的新状态......

但就像我上面所说的那样,对于一个拥有30%总体的随机初始状态,这没有多大意义,因为找不到很多完全死的8 * 8子空间(留下死256 * 256个子空间)

当然,完美优化的方式将持续但并非最不重要,取决于您的语言。

-110

答案 5 :(得分:1)

正如Arbash的黑皮书中所提到的,获得巨大加速的最简单直接的方法之一是保留更改列表。

不是每次都遍历整个单元网格,而是保留您更改的所有单元格的副本。

这将缩小您在每次迭代时必须完成的工作。

答案 6 :(得分:1)

两个想法:

(1)许多配置大多是空的空间。保持活动单元的链接列表(不一定按顺序,需要更多时间),并且在更新期间,仅更新活动单元(这与您的模糊建议OysterD类似:)

(2)保留一个额外的数组,用于存储3个位置(左中右)每行中的活细胞数。现在,当您计算单元格的新死亡/实时值时,您只需要4个读取操作(顶部/底部行和中心侧位置),以及4个写入操作(更新3个受影响的行汇总值,以及死亡/新细胞的实时价值)。这是从8次读取和1次写入略有改进,假设写入不比读取慢。我猜你可能会对这些配置更加聪明,并在这些方面取得更好的改进。

答案 7 :(得分:0)

不知道如何做到这一点,但我记得我的一些朋友不得不用Quadtree代表这个游戏的网格进行任务。我猜它是优化网格空间的真正好处,因为你基本上只代表被占用的单元格。我不知道执行速度。

答案 8 :(得分:0)

这是一个二维自动机,所以你可以查找优化技术。你的想法似乎是压缩你需要在每一步检查的细胞数量。由于您只需要检查被占用单元占用或相邻的单元格,也许您可​​以保留所有此类单元格的缓冲区,并在处理每个单元格时在每一步更新它。

如果您的字段最初为空,则速度会快得多。您可能会发现一些平衡点,维护缓冲区比处理所有单元格的成本更高。

答案 9 :(得分:0)

有针对此的表驱动解决方案,可解析每个表查找中的多个单元格。谷歌查询应该给你一些例子。

答案 10 :(得分:0)

我在C#中实现了这个:

所有单元格都有位置,邻居计数,状态和对规则的访问权。

  1. 将数组B中的所有活动单元格放在数组A中。
  2. 让阵列A中的所有单元格为其邻居计数加1 邻居。
  3. 让阵列A中的所有单元格将自己和它们的邻居放在数组B中。
  4. 阵列B中的所有单元格根据规则及其状态更新。
  5. 阵列B中的所有单元格将其邻居设置为0。
  6. 优点:

    1. 忽略不需要更新的单元格
    2. 缺点:

      1. 4个阵列:网格的2d数组,活动单元的数组和数组 对于活跃的细胞。
      2. 无法处理规则B0。
      3. 逐个处理单元格。
      4. 细胞不仅仅是布尔值
      5. 可能的改进:

        1. 单元格也有“更新”值,只有在没有时才会更新 在当前刻度中更新,不再需要如上所述的阵列B
        2. 而不是阵列B是具有活动邻居的阵列,而阵列B可以是 没有的单元格,以及检查规则B0的单元格。

答案 11 :(得分:-1)

Javascript中最短的实现之一:)

A Game of Life engine Demo in 126 bytes

/* The Game of Life function */
// @param s: current state of the grid
// @param d: size of the grid (d*d)
// @param n: placeholder
// @param k: placeholder
// @param m: placeholder
// @param i: placeholder
// @param j: placeholder
function(s, d, n, k, m, i, j){
  for(
    n = [],                           // Initialize next state
    m = [d + 1, d, d - 1, 1],         // Initialize the upper half of the neighbours indexes
    i = d * d;                        // For each cell
    i--;
    n[i] = k == 3 || s[i] && k == 2,  // Set next state (live if it has 3 neighbours or lives and has 2 neighbours)
    k = 0                             // Reset the count of living neighbours
  )
  for(j in m)                         // for each neighbour position
    k += s[i + m[j]] + s[i - m[j]]    // count the living neighbours
  return(n)                           // return the next state
}