【问题标题】:In Screeps, is CPU limit enforced in a way that allows CPU limit robust code to be written?在 Screeps 中,是否以允许编写 CPU 限制稳健代码的方式强制执行 CPU 限制?
【发布时间】:2016-12-20 23:23:22
【问题描述】:

在 Screeps 中,每个玩家对 CPU 的使用都是有限的,但 documentation for this feature 并没有使这种强制执行的方式足够清晰,以便编写 CPU 限制健壮的代码。我考虑了以下四种可能性:


1。玩家的循环永远不会被打断。

在一种极端情况下,播放器的内存反序列化、主脚本执行和内存重新序列化永远不会中断,超过 CPU 限制仅仅意味着玩家的循环将在后续滴答中被跳过,直到 CPU 债务被偿还。在这种情况下,CPU 限制健壮代码并不是绝对必要的,但它仍然有助于检测玩家的循环何时被跳过并可能开始更有效地做事。这可以使用如下代码轻松实现:

module.exports.loop = function()
{
  var skippedTicks = 0;

  if ( 'time' in Memory )
  {
    skippedTicks = Game.time - Memory.time - 1;
  }

  // Main body of loop goes here, and possibly uses skippedTicks to try to do
  // things more efficiently.

  Memory.time = Game.time;
};

这种管理玩家 CPU 使用率的方式很容易被无限循环滥用,我几乎可以肯定这不是 Screeps 的行为。

2。玩家的循环是原子的。

下一个可能性是玩家的循环是原子的。如果超出 CPU 限制,则玩家的循环被中断,但既不提交预定的游戏状态更改,也不提交对内存的更改。当检测到中断循环时提高效率变得更加重要,因为忽略它意味着玩家的脚本将无法更改游戏状态或内存。但是,检测中断的循环仍然很简单:

module.exports.loop = function()
{
  var failedTicks = 0;

  if ( 'time' in Memory )
  {
    failedTicks = Game.time - Memory.time - 1;

    // N failed ticks means the result of this calculation failed to commit N times.
    Memory.workQuota /= Math.pow( 2, failedTicks );
  }

  // Main body of loop goes here, and uses Memory.workQuota to limit the number
  // of active game objects to process.

  Memory.time = Game.time;
}

2.5。对 Memory 的更改是原子的,但对 Game 对象的更改不是。

编辑:在阅读RawMemory 对象的文档后,我想到了这种可能性。如果脚本被中断,任何已经安排好的游戏状态更改都会提交,但不会提交对内存的更改。考虑到 RawMemory 提供的功能,这是有道理的,因为如果脚本在运行自定义内存序列化之前被中断,则会运行默认的 JSON 序列化,这会使自定义内存序列化更加复杂:自定义反序列化需要能够处理除了自定义序列化编写的任何格式之外的默认 JSON。

3。 JavaScript 语句是原子的。

另一种可能性是播放器的循环不是原子的,但 JavaScript 语句是原子的。当玩家的循环因超过 CPU 限制而中断时,会提交不完整的游戏状态更改和内存更改,但需要仔细编码 - 进行 Screeps API 调用的语句必须将调用结果分配给内存键 - 游戏状态更改和内存更改不会相互不一致。为这种情况编写完全受 CPU 限制的健壮代码似乎很复杂 - 这不是我已经解决的问题,我想在尝试之前确定这是 Screeps 的真实行为。

4。没有什么是原子的。

在另一个极端,甚至单个语句都不是原子的:将 Screeps API 调用的结果分配给内存中的键的语句可能会在调用完成和分配结果之间被中断,并且不完整的提交游戏状态更改和不完整的内存更改(现在彼此不一致)。在这种情况下,编写 CPU 限制健壮代码的可能性非常有限。例如,虽然以下语句写入 Memory 的值的存在无疑表明 Screeps API 调用已完成,但它的缺失并不表明调用未完成无疑:

Memory.callResults[ Game.time ][ creep.name ] = creep.move( TOP );


有谁知道其中哪些是 Screeps 的行为?还是我没有考虑过的其他事情?文档中的以下引用:

CPU 限制 100 意味着在 100 毫秒后执行脚本将被终止,即使它尚未完成某些工作。

暗示可能是案例 3 或案例 4,但不是很令人信服。

另一方面,在模拟模式下的实验结果,其中包含单个蠕变、以下主循环,并在对话框中为无响应脚本选择“终止”:

module.exports.loop = function()
{
  var failedTicks = 0;

  if ( 'time' in Memory )
  {
    var failedTicks = Game.time - Memory.time - 1;

    console.log( '' + failedTicks + ' failed ticks.' );
  }

  for ( var creepName in Game.creeps )
  {
    var creep = Game.creeps[ creepName ];

    creep.move( TOP );
  }

  if ( failedTicks < 3 )
  {
    // This intentional infinite loop was initially commented out, and
    // uncommented after Memory.time had been successfully initialized.

    while ( true )
    {
    }
  }

  Memory.time = Game.time;
};

是因为 failedTicks 达到了它的阈值,creep 只在无限循环被跳过的滴答声上移动。这指向案例 2,但不是结论性的,因为模拟模式下的 CPU 限制与在线不同 - 它似乎是无限的,除非使用对话框的“终止”按钮终止。

【问题讨论】:

  • 我尝试生成 Creep,然后通过无限循环强制超出我的 CPU 限制。我的循环确实终止了,我的小兵最终产卵了。这似乎排除了#1 和#2,但没有区分#3 和#4。

标签: javascript screeps


【解决方案1】:

它不是#1 或#2。我打赌它是#4,最有意义的是监控主循环外部的 CPU 使用情况并在达到限制时将其终止。 #3 将需要 screeps 服务器中的复杂代码来执行“语句级”事务。正如您所发现的,模拟器中没有 CPU 限制。

大多数玩家通过简单地将关键代码放在主循环的早期来解决这个问题,例如首先是塔代码,然后是生成代码,然后是蠕变/工作。这也可以防止代码中出现未捕获的异常,因为您最关键的函数(希望)已经执行。不过,这对于 CPU 限制来说是一个糟糕的解决方案,在我的观察中,一旦您使用了存储桶中的所有 CPU 并不断达到您的常规限制,您的代码似乎每 2 次滴答就会被跳过一次。

我现在没有 CPU 问题(我有订阅),但我会通过将 CPU 密集型代码放在末尾来解决这个问题,如果可能的话,只在你的 CPU 有足够多的时候执行它存储桶,你离你的 500 CPU pertick 的限制还差得很远。拥有更大的小兵也有帮助,寻路甚至只是移动(每次移动 0.2 次)都会占用相当大的 CPU 资源,而更大的小兵意味着更少的小兵。

【讨论】:

    【解决方案2】:

    游戏中的“每日提示”之一说:

    TIP OF THE DAY: If CPU limit raises, your script will execute only partially.
    

    因此我会说,这很可能是 #4
    就像 dwurf 所说,在大多数情况下,以下脚本布局方法应该可以解决问题:

    大多数玩家通过简单地将关键代码放在主循环的早期来解决这个问题,例如首先是塔代码,然后是生成代码,然后是蠕变/工作。 [...]

    【讨论】:

      【解决方案3】:

      默认情况下为案例 4,但可修改为案例 2.5

      正如 nehegeb 和 dwurf 所怀疑的那样,并且对私人服务器的实验已经证实,默认行为是案例 4。提交中断前发生的游戏状态和内存更改。

      但是,服务器主循环对默认 JSON 序列化的运行是由 RawMemory 中存在的未记录键“_parsed”控制的;键的值是对内存的引用。在脚本的主循环开始时删除键并在结束时恢复它的效果是使脚本的主循环所做的整组内存更改原子化,即案例 2.5:

      module.exports.loop = function()
      {
        // Run the default JSON deserialize. This also creates a key '_parsed' in
        // RawMemory - that '_parsed' key and Memory refer to the same object, and the
        // existence of the '_parsed' key tells the server main loop to run the
        // default JSON serialize.
        Memory;
      
        // Disable the default JSON serialize by deleting the key that tells the
        // server main loop to run it.
        delete RawMemory._parsed;
      
        ...
      
        // An example of code that would be wrong without a way to make it CPU limit
        // robust:
      
        mySpawn.memory.queue.push('harvester');
        // If the script is interrupted here, myRoom.memory.harvesterCreepsQueued is
        // no longer an accurate count of the number of 'harvester's in
        // mySpawn.memory.queue.
        myRoom.memory.harvesterCreepsQueued++;
      
        ...
      
        // Re-enable the default JSON serialize by restoring the key that tells the
        // server main loop to run it.
        RawMemory._parsed = Memory;
      };
      

      【讨论】:

        猜你喜欢
        • 2013-02-05
        • 2013-06-03
        • 2018-07-17
        • 1970-01-01
        • 2020-05-21
        • 2012-06-14
        • 1970-01-01
        • 2010-09-19
        • 1970-01-01
        相关资源
        最近更新 更多