【问题标题】:What's an efficient implementation of Conway's Game of Life for low memory uses?对于低内存使用,康威生命游戏的有效实现是什么?
【发布时间】:2011-02-20 20:32:20
【问题描述】:

我正在寻找一种快速且内存效率高的方法来实现 Conway 的生命游戏。

限制条件:96x128 板、大约 2kB 可用 RAM 和 52MHz 处理器(请参阅此处的技术规格:http://www.getinpulse.com/features)。

我当前将每个单元格表示为矩阵中的单个位(96*128/8=1,536 字节)的幼稚解决方案可行,但速度太慢。可以使用哪些技巧来提高性能?

存储活细胞的坐标(例如在这个实现中http://dotat.at/prog/life/life.html)会占用太多内存。

【问题讨论】:

  • 是否可以从显存中读取数据?如果是这样,您最好直接从显示缓冲区访问状态,而不是将其全部复制到内存中。我注意到手表本身有 8kB 的 RAM - 你有什么理由限制在其中的四分之一吗?
  • 似乎最好的解决方案是不要烧掉其余的 RAM 内存,天知道是什么。根据该链接,您有 8kb 的 RAM。尽可能多地释放其他 6kb ......你需要一个相当大的应用程序才能使用这么多,即使你有一个很大的脂肪堆栈,比如说 1kb。为初学者摆脱浮点和动态内存。

标签: embedded conways-game-of-life low-memory


【解决方案1】:

看起来像一个有趣的硬件。 在 96x128 像素显示器的每个像素中存储 1 位会产生 12,288 位。 这已经超过了您所说的 16,384 位“可用”的一半。 如果您甚至无法在每个单元格中存储 2 位,那么就没有太多空间可以做很多事情了。

一些想法:

  • 您有一个 32 位处理器。在这样的处理器上获取位图并计算多个单元的相邻单元数有几个技巧。

  • 存储邻居计数通常更快,出生时增加所有 8 个邻居,死亡时减少所有 8 个邻居,而不是每一代从头开始重新计算邻居的数量 - 但它看起来不像你有足够的内存用于这种方法。

  • 也许您可以使用每个单元格 2x2 像素(因此只有 3,072 个单元格可见)或每个单元格使用 3x3 像素(因此只有 1,376 个单元格可见),这样您的软件工作量就会减少,从而产生运行速度更快的错觉。 (这也释放了足够的 RAM,您可以进行上述邻居计数)。

  • 1234563完全死亡,如果相应区域中的任何单元格还活着,则为“1”。下一代更新只需要查看活区和死区的 1 个单元格边界;您可以跳过检查死区的 6x6 单元核心。此外,如果 4 个最近的相邻区域也死了,您可以完全跳过整个 8x8 单元区域。 1234563相应的 8x8 单元区域在上一代更新中没有变化,如果相应区域中的任何单元在上一次更新中发生变化,则为“1”。下一代更新只需要查看动态区域和静态区域的 1-cell-wide 边界;您可以跳过检查静态区域的 6x6 单元核心,因为它在下一代中将是相同的。
  • 如果模式“足够大”,则基于 Gosper 的 Hashlife 的表示可能能够将其存储在比直接存储位图更少的 RAM 中。唉,我怀疑你远远低于“足够大”的阈值。

【讨论】:

    【解决方案2】:

    在小生命宇宙中,使它们四面环绕(形成环形宇宙)是很常见的,但这需要双重缓冲。在您的情况下,这需要 3KB RAM,但您只有 2KB。

    如果你不换行,那么你就不需要对整个宇宙进行双重缓冲。您只需要在完成使用单元格作为计算的输入之前避免覆盖单元格。

    假设您的宇宙被布置为传统的位图。我们将把它视为一系列在内存中按顺序排列的行。假设宇宙有四行,编号为 0 到 3。

      0
      1
      2
      3
      4
      ...
    

    当您计算下一代时,新版本的第 3 行是使用第 2、3 和 4 行(空白)计算的。您可以将第 3 行的新版本写在第 4 行之上。类似地,从第 1、2、3 行计算新的第 2 行并将其写在第 3 行之上,因为在计算第 2 行之后不再需要该数据。新的第 1 行是根据第 0、1、2 行计算并覆盖第 2 行的。

    这意味着您只需要额外一行的内存。 97*128 位是 1552 字节。

    缺点是你的宇宙在内存中滚动,你必须有一些机制来处理这个问题。每次计算后将其复制回其原始位置(这很慢)或安排能够从内存中的任何位置显示它,并确保您的计算和显示系统可以从高地址到低地址环绕。

    【讨论】:

    • 使用从上到下计算的环形宇宙将需要保留顶行的副本,直到计算底行,但只需要对该行和一个进行双重缓冲或最近计算的两行,而不是整个宇宙。
    【解决方案3】:

    查看 Michael Abrash 在"The Zen of Code Optimization" 中关于生命游戏的章节。有一个实现将 3 个单元的当前和先前状态编码为一个字,并使用查找表和进位位的技巧生成下一个状态。速度非常快。

    【讨论】:

      【解决方案4】:

      我建议从读取板的一行并生成两个新的行缓冲区 H 和 L 的例程开始,这样如果两个或多个 (bin n+1, bit n, bit n-1) 被设置在原始行中,如果在原始行中设置了奇数个 (bin n+1, bit n, bit n-1),则将设置 L 的 bit 'n'。

      总共分配三对行缓冲区(称它们为 H0..H2 和 L0..L2)。获取源文件的每一行并计算一对缓冲区并将其存储在一个 HL 对中,保留它和前两个。检查所有六个缓冲区中的一个词将揭示原始矩阵中的 32 个单元格中哪些应该是活的,当且仅当它们以前是,哪些应该是活的,无论以前的状态如何。

      为了在机器代码中获得最佳性能,三对缓冲区可以交错;这可以实现每个像素少于两个周期的执行速度。可能在 52MHz 时过大(只有 12288 像素,帧速率约为 4000fps),但速度很酷。

      【讨论】:

        【解决方案5】:

        如果您有许可滥用设备上剩余的 30KB(也称为闪存),则可以始终将其存储在那里,这并不理想,但这是一种潜在地解决有限 RAM 的方法。

        从效率上讲,您将相互交换 CPU 和内存性能:

        就内存效率而言,位数组可能是最佳解决方案,但您会因为遍历该网格而损失 CPU 效率。

        根据内存地址的功能和大小,您可以使用单元的链接列表来避免遍历整个网格:绝对可以节省您扫描大量死单元区域的时间。

        【讨论】:

        • 我认为在这种情况下实际上没有时空权衡。将单元状态存储为单个位允许您使用处理器按位整数运算的固有并行性,而使用其自己的整数原语存储每个单元状态会在高阶位上带来计算的沉没成本。也就是说,我还没有实现该技术,因此可能存在降低性能增益的开销。将单元状态存储在单个位中的另一个潜在性能优势是提高缓存效率。
        【解决方案6】:

        坚持使用位数组并使用此技巧跳过邻居计数:创建一个查找表,其中包含创建或维护活动单元的相邻单元块的可能位模式。

        如果你想最小化内存,“索引”模式可以被打包成 8 位:3 位从上面的行开始,2 位从列到两侧,3 位从下面的行开始。您可以将输出编码为查找表中的单个位,总共仅占用 256 位。使用索引作为查找数组的位移计数来“计算”结果。位移和算术 OR 运算仍然非常快,并且这种技术消除了动态计算相邻活细胞的问题 - 或者更确切地说,查找表对计数和计算进行了预处理。

        首要的处理瓶颈应该是:检查板边条件,即行尾;单词边界;提取和打包相邻位作为索引值。使用 32 位处理器,您应该能够在到达字边界之前非常快速地循环通过 30 个单元。如果将位打包成字大小的整数,则寻址单元行可能只是添加列数/32 的问题。将结果缓存到两个备用的新生命行中,并在处理完一个后复制一整行。

        您可以利用一些模式对称性来进一步优化事物,但这可能就足够了。我认为这种技术将使处理和内存使用保持在您的限制范围内。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-02-28
          • 2017-04-14
          • 1970-01-01
          • 2013-02-21
          • 1970-01-01
          • 1970-01-01
          • 2010-09-07
          相关资源
          最近更新 更多