循环中的循环时间抖动

时间:2012-01-17 21:07:33

标签: loops profiling

我在制作游戏时遇到了一些问题,虽然问题不仅限于游戏编程,但我会用我遇到的具体例子来表达:

正如预期的那样,我在循环中运行游戏代码,控制游戏逻辑并在每个循环中绘制一个帧。但是,在完成每个周期所花费的时间内,我遇到了相当多的抖动。正常循环时间在最短和最长测量时间之间变化大约两倍,有时(更少见),出现需要花费大量时间的较大尖峰。

我想消除两者。我想消除正常的抖动,使绘图更平滑,我想消除尖峰,因为它们是显而易见的,令人讨厌。问题是,我不知道是什么代码导致它们出现。我已经尝试过测量一些特定的代码路径或收集系统资源的使用情况,但到目前为止它只是帮助。

喜欢做的事情是能够运行CPU-profiler,每个周期重置其计数器数据。这样,我可以轻松地在帧与帧之间进行比较,其中花费时间以及哪些代码路径本身或多或少地抖动。但是我该怎么做?我遇到了Java和C中的问题,并且Java的-Xprof选项和GCC的gprof程序仅在程序生命周期的开始到结束时打印测量时间(如预期的那样)。有什么方法可以在每个周期中转储和重置这些分析器的数据和/或是否有其他我可以尝试提供该功能的分析器?我还认为它们都只需要大约100个样本/秒,当帧只需要大约10-20毫秒时,分辨率非常低。有没有办法提高他们的采样率?

或者,当然 - 我还有其他一些方法可以解决这些问题吗?

3 个答案:

答案 0 :(得分:1)

让我们考虑C代码,因此我们不包括GC时间。

我认为每一帧都没有发生超时,但很少,所以任何只测量聚合时间的探测器都不会告诉你太多。

我会做什么(这可能并不容易)只是在程序运行时尝试告诉程序正在做什么,而不是所有时间。 如果可能的话,在每个帧的开头,我会设置一个闹钟定时器,设置为在足够的毫秒后关闭,以便在那个额外的时间内触发某个地方。 然后在计算帧结束时,我会禁用定时器,以便在帧完成后不会关闭。 向计时器添加一个小的随机数毫秒可能很有用。

然后当计时器熄灭时,我会收集一个堆栈样本并研究它以查看程序正在做什么。

可能很难收集样本并将其保存起来以后再查看。 在闹钟响起时简单地输入调试器可能更容易,也更有用,并且研究堆栈以及当时程序正在做什么。 要获得另一个样本,要么只是恢复执行,要么从头开始重新开始。

关键在于,你想知道它在做什么以及为什么它会在那段时间内完成它。 你希望它所做的多余工作,或其中一些工作正在那段时间内发生。

在看到有趣的内容之前,您最多可能需要20个样本, 但是,只要多个样本显示您没有真正意识到它正在发生的事情,并且您可以摆脱,这将减少过多的时间使用量。 在看到之前你必须采取的样本越少,时间就越少。

可能存在多个这样的问题,所以在找到第一件事之后不要停止。 一遍又一遍地做,直到你找不到任何你可以删除的东西。

答案 1 :(得分:0)

我确信有更简单的方法 - 但这就是我们这样做的方式: 编写自己的(非常简单的)profiler-timer(带有开始/结束宏),它也接受一个阈值时间,并在它超过给定阈值时进行记录。

用此包装游戏循环中的每个顶级组件。通过这种方式,您可以获得哪些组件是瓶颈的日志。

识别组件后,您可以使用相同的方法深入查看代码。

这需要一些手动工作(并且可能有工具可以做到这一点),但很快你就可以到达问题的确切位置。

例如(伪代码):

while (game-loop):

  START_PROFILE(PHYSICS, 0.05);
  UpdatePhysics();
  END_PROFILE(PHYSICS);

  START_PROFILE(NETWORK, 0.05);
  UpdateNetwork();
  END_PROFILE(NETWORK);

  START_PROFILE(RENDER, 0.05);
  Render();
  END_PROFILE(RENDER);

答案 2 :(得分:0)

我想通过建议让你的个人资料例程记录自最后一帧开始以来在每个个人资料单元中花费了多少时间来提及Asaf的答案

I>。我不知道现有的配置文件工具有多么容易做到这一点,但如果你自己旋转......

void START_PROFILE(PROFILE_REC *rec)
{
  if (rec->frame != total_frame_count)
  {
    rec->frame = total_frame_count;
    rec->time_before_frame = rec->total_time;
  }
  rec->time_before_last = rec->total_time;
  rec->start_time = get_precise_system_time();
}
void END_PROFILE(PROFILE_REC *rec)
{
  rec->total_time += get_precise_system_time - rec->start_time;
}

上面没有任何错误检查,以确保END_PROFILE调用平衡START_PROFILE调用;如果调用不匹配,它将记录无意义的数字,但如果没有错误报告,错误检查可能没有用,并且错误报告会增加另一层复杂性。

顺便提一下,如果你有一些任务可以在准确的时间内容忍相当大的抖动,那么在每一帧上完成所有必须在那个帧上完成的东西然后做可以做的事情可能会有所帮助。完成“随时”直到下一帧到达。然后处理该帧,一旦完成,返回处理“随时”的任务。根据您的喜好,您可以使用功能齐全的多任务操作系统,简单的循环堆栈切换器,或一组状态机,或其他任何方式。我已经在具有128字节RAM的游戏机上使用了这种方法,对于帧来说甚至几微秒“迟到”会导致可见的显示中断,并且它可以非常有助于平衡时间的变化。