C#性能分析器显示长时间暂停,无法从提供的数据中确定原因。

23

当渲染帧时,我的C#应用程序出现了意外的峰值。我一直在使用分析器进行检查,发现以下情况:

  • 当垂直同步打开时,程序似乎在交换缓冲区时将控制权交给操作系统(或执行某些操作)...并以某种方式保持其渲染非常一致。这很好,因为它可以平稳地渲染帧,而没有这种奇怪的微跳。下面的图像显示被认为是完全流畅的情况。这是我们所说的“通过平稳渲染而没有任何卡顿,它完美地工作”的情况。

VSync image

当垂直同步关闭并且程序不断地写入数据而没有任何休息时,尽管每秒帧数更高,但似乎存在这些大的空白区域,什么也没做。我相信这就是游戏中出现的微卡顿,因为它会导致FPS急剧下降到60fps以下。下面的图片显示了出现问题的情况:

Microstutter

在上述情况下,Δ时间为21毫秒(超过每秒60帧的16.6毫秒),导致游戏明显卡顿。更糟糕的是,它只在此之后开始渲染(如您在之后看到的实心矩形所示),因此这21毫秒是虚假的,根据图表显示,实际上更像是40毫秒,非常糟糕。
当垂直同步开启时,这种大间隔永远不会发生,原因我不理解。对于不熟悉垂直同步和游戏的人来说,你不能在第一人称射击游戏中使用垂直同步,因为它会破坏输入处理方式,所以我不能使用垂直同步,必须调查非垂直同步版本。我需要找出为什么非垂直同步版本会有这些主要停顿。
我的问题是,如何从上面看到的图像中知道是什么导致了这个延迟?
性能分析器显示,在此处有大量等待时间,等待时间高达80%,CPU使用率为20%(与准备渲染数据时的100% CPU使用率相比)。
分析器还显示,在该循环中根本没有运行任何渲染代码...这很奇怪,因为渲染器几乎完全占据了性能,因此在没有任何帧速率限制的情况下,它应该用实心蓝色矩形淹没整个图表。
问题是,分析器显示的代码只是调用我上面选择的区域中的轮询输入和dll。

DLL calls

请注意,DispatchRenderFrame 另一个调用正在进行 OpenGL 调用,但当我完全删除它时,程序没有任何影响,因此您可以忽略它。这可能意味着下面看到的用户输入对微卡顿问题也没有影响......但我不能删除它,因为它是我用于窗口管理(OpenTK)的库的一部分。
我不确定 CLR Worker 线程是什么或者它在做什么。它也发生在垂直同步(期望平滑的 profiling one),所以即使我不知道它是否是罪犯,我的猜测是它可能不是,但我不确定,因为它在 'desired vsync example' 中的相同位置也出现了。
是否有某些中断发生,操作系统接管了但没有恢复我的线程,因为它将其分类为 CPU 占用率过高?只是一种想法......但再次在示例中显示蓝色条,因此我假设 Main 线程实际上没有在我突出显示的代码段中休眠,而是实际运行?
正如您所看到的,我不确定我突出显示的时间片正在告诉我什么,这就是我需要帮助的地方。我不知道为什么等待百分比在代码的这一点上如此之高,或者从这里去哪里进一步诊断问题。
编辑: 我使用 Stopwatch 类进行了粗略的分析,以查看哪些位置会导致波峰,并且波峰仅来自计算部分。没有任何 OpenGL 调用会导致任何延迟。它纯粹发生在一个函数的函数内部,这些函数进行数学计算和访问数据,或将数据写入预先分配的结构体数组中。

额外备注:

这也让我好奇是否有一种方法可以强制使C#虚拟机尽可能地使用CPU?

没有垃圾被产生。这不是一个GC问题。我不能在应用程序运行期间运行GC,所以我不会产生垃圾。渲染函数中的所有结构体都在堆栈上,唯一需要管理对象的堆分配数组是具有足够大的大小来存储所有渲染数据的池化数组。它只在开始和结束时出现在分析器中,但不会在渲染阶段运行。


@iSR5 我会编辑我的回答,因为我要说的话(我认为我遗漏了这些信息,我的错):暂停似乎发生在渲染计算期间,但在任何实际的OpenGL调用之前,由于某种原因,主线程似乎在几何体的原始计算中进入睡眠状态,并且不会切换回来。我已经将代码隔离到它发生的位置,但我无法解释为什么C# VM在连续渲染通行证上使用完全相同的数据和指令时会停顿。这可能会改变您的响应。 - Water
1
值得注意的是:A)结构体不能保证在堆栈上...它们倾向于在堆栈上,但运行时可能将它们移动到其他地方。B)如果您没有通过“ref”传递它们,则它们肯定会被移动到堆上。 - Mgetz
4
@Mgetz,这是不正确的。当通过按值传递结构体参数而没有使用 ref/in 时,它们不会被移动到堆上,而是使用线程的栈进行复制。 - dymanoid
1
在桌面CLR上的C#的Microsoft实现中,当值类型是局部变量或临时变量且不是lambda或匿名方法的闭合局部变量,并且方法体不是迭代器块且Jitter选择不将该值寄存器化时,值类型存储在堆栈上。 https://blogs.msdn.microsoft.com/ericlippert/2010/09/30/the-truth-about-value-types/ - MineR
也许尝试插入一个计时器并经常记录经过的时间(最好记录到一些大型预定义的内存缓冲区),并与分析器数据进行比较(也许我正在错误地读取分析数据,但对我来说看起来代码似乎只是在等待某些东西而不是运行)。你是如何进行轮询的?你运行任何库代码吗?它是否阻塞?我看到有一个选择了21ms间隔,但实际上只有8.3ms在运行。在这方面,ProcessEvents看起来非常可疑。 - mostanes
显示剩余21条评论
2个回答

1

当出现卡顿时,您的应用程序窗口是否被Windows 10任务栏遮挡?我发现这种现象与显着的卡顿有关。我认为半透明任务栏的渲染意味着创建离屏缓冲区,这会干扰任何关键帧渲染循环。


-1

这不是针对你的问题的答案,而只是关于分析结果中的一些奇怪现象

我从我们私有的GitHub问题中复制了它

为了明确起见,这个答案的信息是,你应该仔细考虑分析结果,因为提供的分析结果似乎有一些奇怪的值,所以我只是提到可能问题出在分析操作上。


因为线程池中有多个线程,等待时间会被计算多次,所以我们会得到这些奇怪的结果。

例如,对于之前的测试#255(评论), 我们只在主线程中执行所有的启动,有主线程和终结器。

enter image description here

但是在当前的测试中,我们有许多线程池,而分析器会累积所有这些线程池,尽管它们同时执行,但这并不是实时的,这是一件疯狂的事情,但这就是发生的事情。

enter image description here

所以如果我们只选择主线程,我们会得到正确的结果

enter image description here

但是要获取实际的配置而不是真实的,对于SetParameter异步方法所需的时间,我们必须进入其线程并选择其类别

enter image description here


感谢您尝试帮助回答问题。 - Water

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接