在 Screeps 中,CPU 限制是否以允许编写 CPU 限制健壮代码的方式执行?

In Screeps, is CPU limit enforced in a way that allows CPU limit robust code to be written?

在 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 限制,则玩家的循环会被打断,但既不会更改预定的游戏状态,也不会提交对 Memory 的更改。当检测到中断循环时提高效率变得更加重要,因为忽略它意味着玩家的脚本将无法更改游戏状态或内存。然而,检测中断周期仍然很简单:

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。对内存的更改是原子的,但对游戏对象的更改不是。

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

3。 JavaScript 语句是原子的。

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

4。没有什么是原子的。

在另一个极端,即使是单个语句也不是原子的:将 Screeps API 调用的结果分配给内存中的键的语句可能会在调用完成和分配结果,并且提交了不完整的游戏状态更改和不完整的内存更改(现在彼此不一致)。在这种情况下,编写 CPU limit 健壮代码的可能性非常有限。例如,尽管通过以下语句写入内存的值的存在毫无疑问表明 Screeps API 调用已完成,但它的缺失并不表示毫无疑问调用未完成:

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


有谁知道其中哪一个是 Screeps 的行为?或者是其他我没有考虑过的东西?以下引用自文档:

The CPU limit 100 means that after 100 ms execution of your script will be terminated even if it has not accomplished some work yet.

暗示可能是案例 3 或案例 4,但不是很有说服力。

另一方面,在模拟模式下使用单个 creep、以下主循环并在对话框中为无响应脚本选择 'Terminate' 的实验结果:

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 只在无限循环被跳过的 ticks 上移动。这指向案例 2,但不是决定性的,因为模拟模式中的 CPU 限制与在线不同 - 它似乎是无限的,除非使用对话框的 'Terminate' 按钮终止。

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

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

我现在没有 CPU 问题(我有订阅),但我会通过将 CPU 密集型代码放在最后来解决这个问题,如果可能的话,只当您的存储桶中有足够的 CPU 并且距离您的 500 CPU 每笔报价限制还差得很远时执行它。更大的 creep 也有帮助,无论是寻路还是只是移动(每次移动 0.2)占用相当大的 CPU 都是很常见的,而更大的 creep 意味着更少的 creep。

其中一个游戏 'tips of the day' 说:

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

所以我想说,很有可能 #4!
正如 dwurf 所说,在大多数情况下,以下脚本布局方法应该可以解决问题:

Most players solve this problem by simply putting critical code early in their main loop, e.g. tower code comes first, then spawn code, then creep movement/work. [...]

默认为案例 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;
};