优化康威的“生命游戏”

Posted

技术标签:

【中文标题】优化康威的“生命游戏”【英文标题】:Optimizing Conway's 'Game of Life' 【发布时间】:2010-09-07 14:48:01 【问题描述】:

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

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

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

您对如何优化(以提高速度)生命游戏迭代有任何想法(或经验!)?

【问题讨论】:

参见:hashlife、golly 和 Alan Hensel 的 java 算法。 【参考方案1】:

如果您不想要太复杂的东西,那么您可以使用网格将其分割,如果网格的那部分是空的,请不要尝试模拟它(请查看 Tyler 的答案)。但是,您可以进行一些优化:

    根据活细胞的数量设置不同的网格大小,因此如果活细胞不多,则可能意味着它们位于一个很小的地方。 当你随机化它时,在用户更改数据之前不要使用网格代码:我亲自测试过随机化它,即使经过很长时间,它仍然会填满大部分棋盘(除非足够小的网格,在这一点上它不会有那么大的帮助) 如果要将其显示到屏幕上,请不要使用像素大小 1 和 2 的矩形:而是设置输出的像素。任何更高的像素大小,我发现使用本机矩形填充代码是可以的。此外,预设背景,这样您就不必为死细胞填充矩形(不是活细胞,因为活细胞很快就会消失)

【讨论】:

【参考方案2】:

我在 C# 中实现了这个:

所有单元格都有一个位置、一个邻居计数、一个状态以及对规则的访问权限。

    将阵列 B 中的所有活细胞放入阵列 A。 让数组 A 中的所有单元格在其相邻单元格的数量上加 1 邻居。 让数组 A 中的所有单元格将自己及其相邻单元格放入数组 B。 Array B 中的所有单元格都根据规则及其状态进行更新。 Array B 中的所有单元格都将其邻居设置为 0。

优点:

    忽略不需要更新的单元格

缺点:

    4 个阵列:一个二维阵列用于网格,一个阵列用于活细胞,一个阵列 用于活动单元格。 无法处理规则 B0。 逐个处理单元格。 单元格不仅仅是布尔值

可能的改进:

    单元格也有一个“更新”值,只有在没有更新时才会更新 在当前刻度中更新,消除了上面提到的数组 B 的需要 阵列 B 不是具有活动邻居的阵列,而是阵列 B 没有的单元格,并检查规则 B0。

【讨论】:

【参考方案3】:

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

章节17 和18 的 Michael Abrash 的Graphics Programmer's Black Book 是其中之一 我读过的最有趣的书 有。这是一堂思考的课 盒子外。整本书是 真的很棒,但最终优化 生命游戏的解决方案是 令人难以置信的编程。

【讨论】:

@Chris: 到 byte.com 的链接现在已经失效 :( 我修复了指向 gamedev.net 的链接。【参考方案4】:

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

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

我认为,当您的初始状态类似于“随机,但生存机会低于 5%”时,首先检查完全死空间是有意义的。

我会将矩阵分成两半,然后先检查较大的。

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

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

如果其状态总和 >0,您现在将第二季度再次分成 4 部分 - 并对每个子空间重复此检查。

您现在可以降低到 8*8 或 10*10 的子帧(不确定这里什么最有意义)。

每当你发现生命时,你就将这些子空间标记为“有生命”。

只有“有生命”的空间需要被划分成更小的子空间——空的可以跳过。

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

您可能认为将 10,000*10,000 空间划分为 8*8 的子空间是很多操作系统任务 - 但实际上累积它们的状态值比对每个单元执行 GoL 算法加上它们的计算工作要少得多8 个邻居加上比较数量并在某处存储网络迭代的新状态...

但就像我上面所说的,对于一个具有 30% 人口的随机初始化状态,这没有多大意义,因为不会有太多完全死掉的 8*8 子空间可以找到(别管死的 256*256 子空间)

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

-110

【讨论】:

【参考方案5】:

算法本身本质上是可并行的。在未优化的 CUDA 内核中使用相同的双缓冲方法,在 4096x4096 的包装世界中,我每代的时间大约为 25 毫秒。

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

两个想法:

(1) 许多配置大多是空白的。保留活细胞的链接列表(不一定按顺序,这将花费更多时间),并且在更新期间,仅在活细胞周围更新(这类似于您的模糊建议,OysterD :)

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

【讨论】:

【参考方案8】:

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

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

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

【讨论】:

【参考方案9】:

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

在这里查看:

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

还有 XLife:

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

【讨论】:

【参考方案10】:

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

【讨论】:

使用模板元编程进行预计算会很有趣,而不是显式编码。【参考方案11】:

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

如果您的字段最初是空的,这会更快。您可能会找到一些平衡点,在该点维护缓冲区比处理所有单元格的成本更高。

【讨论】:

【参考方案12】:

不完全知道如何做到这一点,但我记得我的一些朋友不得不用四叉树来表示这个游戏的网格来分配任务。我猜这对于优化网格空间非常有用,因为您基本上只代表被占用的单元格。不过我不知道执行速度。

【讨论】:

以上是关于优化康威的“生命游戏”的主要内容,如果未能解决你的问题,请参考以下文章

为啥康威的生命游戏可以归类为万能机?

康威的生命游戏:为啥模式行为不正确?

康威生命游戏的简单动画与 FuncAnimation

ruby 康威的生命游戏 - 版本1

ruby 康威的生命游戏

Python案例:协程实现康威生命游戏,元胞自动机