使用Visual Studio 2015中的GPU 使用率工具对常见DirectX性能问题进行可视化调试

[原文发表地址]:Visualizing Common DirectX Performance Problems with GPU Usage in Visual Studio 2015

[原文发表时间]:2016/2/4 9:12 AM

我们努力在即将发布的Visual Studio 2015 Update2中添加了些新的功能,并对GPU 使用率工具的一些问题进行了修复。对于那些不熟悉GPU 使用率工具的开发者而言,GPU 使用率工具是一个轻量级的捕获工具,它为我们提供在运行DirectX 游戏时,CPU和GPU上的相关工作量信息,以及在GPU上执行每一个DirectX调用所花费的时间信息。

GPU 使用率工具能够输出很多的数据,所以我想先介绍下如何在DirectX应用程序中诊断和调查常见的性能问题。

运行正常

在我们调查性能问题之前,我们需要知道我们正常运行一个应用程序的基准线是什么。如我所用的我们这里楼下一个团队提供的例子MiniEngine 。最酷的是,MiniEngine 是一个基本完整的游戏引擎,它也足够小,很容易在代码上进行有趣的调整和实验。

让我们从包含MiniEngine的ModelViewer项目开始吧,我使用Alt+F2启动了诊断工具会话。打开GPU 使用率,运行10秒左右,收集如下的一条日志。

从上面的概述中很容易看出这个应用程序在我的系统上运行正常。帧率稳定在60FPS,并且GPU利用率保持在50%左右。如果我们在这个日志上选择一段时间,我们可以进一步挖掘出高性能是什么样的。

值得一提的是,在Update1中我们对GPU 使用率进行了修改,它显示了一个通过进程和线程分解的GPU分析视图,而不像我们在Visual Studio 2015 RTM中使用的用处不大的内核视图。如果你需要看到系统上的所有进程,只需要从进程下拉框中选择“All”,所有不同内核的进程将显示出来。

首先要看的是上图中细灰色的线,它向下和各种时间线栏交织。这些是来自于你的监听器V-sync 事件。要检查你的帧率,你只需看一下是否能得到每个显示给用户的帧的当前事件。当前事件在每栏中被显示成标记“P”(这个标记除非你把它放大,否则可能看不到)。你也可以在时间线控制顶上面的过滤框中仅输入“Present”。如此之后,过滤器将只列出Present事件,你可以用箭头向下遍历事件列表查看所有的Presents事件。

在我们上面的例子中,我们调整了所有的CPU进程和所有的GPU进程到大约16.6毫秒去创建每一帧。正因这样,每帧能随着v-sync事件按时出现,同时我们能够获得玩家所期待的像黄油一样平滑的60fps。

CPU 部分

那么如果这个应用程序运行并不理想,情况会怎样?在对代码进行了一些调整后,新收集的日志如下所示。

我们的帧时间现在已经超过图表所能显示的范围,我们的FPS也差不多在30以下浮动,而且我们的GPU利用率实际上也已经降低了。这里我们看到的是CPU问题的经典症状。此处我们的CPU有太多的工作要做,所以它无法及时地给GPU发送指令。GPU一直处于等待状态而CPU却在疯狂地旋转。如果我们进入详细信息页面,可以更加清楚地看到这种不平衡。

在GPU栏中,工作块和正常情况下的大小相同。但是在CPU栏中,我们现在有一个线程(那个引起了我们渲染命令的问题的线程)完全占据了CPU。Present调用与v-sync分隔开来,所以我们开始丢帧,而且我们的游戏对玩家而言开始变得不连贯起来。注意:GPU 3D栏显示的一些蓝色小标记实际上是devenv.exe,它是Visual Studio进程,在GPU中做一点工作。

当我们知道我们正在处理一个CPU问题时,我们需要查看在CPU上我们的时间是如何花费的。当分析出这个应用程序正在做的是非图像相关的CPU工作时,我们就可以把这段时间减去。或者使用一些像DirectX12这样的,具有更强的能力——使用命令列表在多线程中分解渲染代码的技术,使得CPU不断地给GPU提交工作。

把GPU使用率工具与其他Visual Studio诊断工具相集成的一个好处是,你可以协同运行不同的工具,方便查看其他方面。我们的GPU使用率工具简单地展示了CPU上活跃着的线程和进程,但并不包括在那段时间中发生了什么。回到诊断工具页(Alt+F2)我可以选择GPU和CPU 使用率然后同时运行它们。

现在在我查看GPU使用率详细信息的时间段里,我也可以查看在CPU上发生的一切。看起来大量时间花在了我的每帧更新阶段中对一个特定枚举类型的AI处理上。

GPU 部分

下图是MiniEngine的另一个运行,中途我增加了一些设置。从这个概况可以清晰地看到我们的性能从60fps掉到30fps的区域,并且在这个过程中我们看到一个相应的GPU利用率增高。

在这个概况中可以明显地看出性能的降低,但是实际上是什么造成了这个性能变化呢?在上述情形中,打开两段不同的详细信息对我们的分析工作是非常有帮助的,一段是从这个应用程序执行正常时开始,另一段是从不正常时开始。

在左图中我们可以看到GPU工作块均匀分布在每一个v-sync中。同时在右图中GPU栏完全是满的而且Present事件也没有在每一个v-sync间隔中。虽然这也是有帮助的,但是在这一点上我们也可以从一个简单的FPS计数器就得到很多类似的信息。如果想要使这个对比更有用,我们可以详细分析时间线上的GPU 使用率块或事件列表,更具体地弄清楚GPU的时间到底消耗在什么地方。事件列表中的DirectX事件最初将只以扁平列表显示,或者在DirectX12中以CommandList分组显示。在如此大的列表中查明每一个调用在做些什么是非常棘手的一件事,所以为了充分利用GPU 使用率,最好在你的代码的适当位置加一些标记事件。当这些事件到达GPU时,GPU 使用率将会在UI中把它们按等级分组,并将如下示例所包含的DirectX事件的持续时间收起来。对于DirectX11查看BeginEvent API,DirectX12查看PIXSetMarker API。有了这样的分层,你就可以在渲染代码的不同区域深层分析时间消耗了。

在以上的例子中,我们展示了在MiniEngine渲染代码的主渲染部分的大量时间消耗。随着我们在事件列表中选择事件或者对其分组,顶上的时间线将更新并显示这个事件的时间量,如果这个事件从CPU调用的话,我们也可以在CPU栏看到类似的。因此你可以看到从API在CPU上被调用,到GPU队列实际上开始着手执行那段代码而产生的延迟。

通过查看写有注释的代码好的和坏的部分,找到由于修改代码或者资源引起的性能问题,是轻而易举的。

如果使用过滤器选项,在事件列表中导航不方便的话,列排序能够帮助调整你看到的数据。下图是以GPU Duration排序的分组事件,而非以执行时间排序的所有”Decompress and downsample”事件实例。

我将会在今后的博客中进一步介绍一些其他在Update1 和Update2 中加入的GPU 使用率工具新功能。