
游戏开发者的性能分析技巧
流畅的性能对于为玩家创造沉浸式游戏体验至关重要。通过分析和优化游戏在各种平台和设备上的性能,您可以扩大玩家基础并增加成功的机会。
本页面概述了游戏开发者的一般性能分析工作流程。摘自电子书,终极Unity游戏性能分析指南, 可免费下载。这本电子书由外部和内部的Unity游戏开发、性能分析和优化专家共同创建。
继续阅读以了解设置性能分析的有用目标、常见的性能瓶颈,例如CPU受限或GPU受限,以及如何更详细地识别和调查这些情况。

设定帧预算
以每秒帧数(fps)测量游戏的帧率并不理想,因为这无法为您的玩家提供一致的体验。考虑以下简化的场景:
在运行时,您的游戏在0.75秒内渲染了59帧。然而,下一帧的渲染需要0.25秒。平均交付帧率为60 fps听起来不错,但实际上玩家会注意到卡顿效果,因为最后一帧需要四分之一秒来渲染。
这就是为什么每帧的特定时间预算很重要的原因之一。这为您在分析和优化游戏时提供了一个坚实的目标,最终为您的玩家创造了更流畅和更一致的体验。
每帧的时间预算将基于您的目标fps。一个目标为30 fps的应用程序每帧的时间应始终少于33.33毫秒(1000毫秒/ 30 fps)。同样,目标为60 fps的每帧时间为16.66毫秒(1000毫秒/ 60 fps)。
在非交互序列中,您可以超出此预算,例如在显示UI菜单或场景加载时,但在游戏过程中不能超出。即使是超过目标帧预算的单个帧也会导致卡顿。
注意:在VR游戏中保持一致的高帧率对于避免导致玩家恶心或不适至关重要。没有它,您在游戏认证过程中可能会被平台持有者拒绝。
每秒帧数:一个具有误导性的指标
玩家衡量性能的常见方式是帧率或每秒帧数。然而,建议您使用毫秒为单位的帧时间。要理解原因,请查看上面的fps与帧时间的图表。
考虑这些数字:
1000毫秒/秒 / 900 fps = 每帧1.111毫秒
1000毫秒/秒 / 450 fps = 每帧2.222毫秒
1000毫秒/秒 / 60 fps = 每帧16.666毫秒
1000毫秒/秒 / 56.25帧每秒 = 每帧17.777毫秒
如果您的应用程序以900帧每秒运行,这意味着每帧的时间为1.111毫秒。在450帧每秒时,这为每帧2.222毫秒。这仅代表每帧1.111毫秒的差异,即使帧率似乎下降了一半。
如果您查看60帧每秒和56.25帧每秒之间的差异,这分别转换为每帧16.666毫秒和17.777毫秒。这也代表每帧多出1.111毫秒,但在这里,帧率的下降在百分比上感觉远不那么戏剧化。
这就是为什么开发人员使用平均帧时间来基准游戏速度而不是帧每秒。
除非您低于目标帧率,否则不要担心帧每秒。专注于帧时间来测量您的游戏运行速度,然后保持在您的帧预算内。
阅读原始文章“罗伯特·邓洛普的帧每秒与帧时间”以获取更多信息。

移动挑战
热控制是开发移动设备应用程序时需要优化的最重要领域之一。如果CPU或GPU由于设计不合理而长时间以全速工作,这些芯片会变热。为了避免对芯片造成损坏(并可能烧伤玩家的手!),操作系统将降低设备的时钟速度以使其冷却,从而导致帧卡顿和糟糕的用户体验。这种性能降低被称为热节流。
更高的帧率和增加的代码执行(或DRAM访问操作)会导致电池消耗增加和热量产生。糟糕的性能还可能使低端移动设备的整个部分失效,这可能导致错失市场机会,从而降低销售。
在解决热问题时,请考虑您可以使用的预算作为系统范围的预算。
通过利用早期分析技术来优化您的游戏,从一开始就对抗热节流和电池消耗。为您的目标平台硬件调整项目设置,以应对热量和电池消耗问题。
在移动设备上调整帧预算
将帧空闲时间保持在大约35%是应对设备在长时间游戏中出现热问题的典型建议。这给移动芯片提供了冷却的时间,并有助于防止过度耗电。使用每帧33.33毫秒的目标帧时间(对于30 fps),移动设备的典型帧预算大约为每帧22毫秒。
计算如下:(1000 ms / 30) * 0.65 = 21.66 ms
要在移动设备上实现60 fps,使用相同的计算需要目标帧时间为(1000毫秒/60) * 0.65 = 10.83毫秒。在许多移动设备上实现这一点是困难的,并且会使电池耗电速度是目标30 fps的两倍。出于这些原因,大多数移动游戏目标是30 fps而不是60。使用Application.targetFrameRate来控制此设置,并参考电子书中的“设置帧预算”部分以获取有关帧时间的更多详细信息。
移动芯片上的频率缩放可能会使在分析时识别帧空闲时间预算分配变得棘手。您的改进和优化可能会产生净正面效果,但移动设备可能会降低频率,从而导致运行更冷。使用自定义工具,如FTrace或Perfetto来监控移动芯片的频率、空闲时间和缩放情况,优化前后均可使用。
只要您保持在目标fps的总帧时间预算内(30 fps为33.33毫秒),并看到设备工作量减少或记录较低的温度以维持此帧率,那么您就走在正确的轨道上。
在移动设备上增加帧预算的余地的另一个原因是考虑到现实世界的温度波动。在炎热的日子里,移动设备会升温并难以散热,这可能导致热节流和糟糕的游戏性能。留出一部分帧预算将有助于避免这些情况。

减少内存访问操作
DRAM访问通常是移动设备上耗电量大的操作。Arm的针对移动设备图形内容的优化建议表示,LPDDR4内存访问的成本大约为每字节100皮焦耳。
通过减少每帧的内存访问操作数量来减少内存访问。
- 降低帧率
- 在可能的情况下降低显示分辨率
- 使用简化的网格,减少顶点数量和属性精度
- 使用纹理压缩和mipmap
当需要关注利用Arm或Arm Mali硬件的设备时,Arm Mobile Studio工具(特别是Streamline Performance Analyzer)包括一些很好的性能计数器,用于识别内存带宽问题。这些计数器为每个Arm GPU代列出并解释,例如Mali-G78。请注意,Mobile Studio GPU分析需要Arm Mali。
建立硬件层级以进行基准测试
除了使用特定平台的分析工具外,为每个平台和您希望支持的质量层级建立层级或最低规格设备,然后对这些规格进行性能分析和优化。
例如,如果您针对移动平台,您可能决定支持三个层级,质量控制根据目标硬件切换功能。然后您针对每个层级中的最低设备规格进行优化。作为另一个例子,如果您正在为PlayStation 4和PlayStation 5开发游戏,请确保在两者上进行分析。
有关完整的移动优化指南,请查看优化您的移动游戏性能。这本电子书有许多技巧和窍门,可以帮助您减少热限制并延长运行您游戏的移动设备的电池寿命。
从高到低级别的性能分析
从上到下的方法在分析时效果很好,从禁用深度分析开始。使用这种高层次的方法收集数据,并记录哪些场景导致不必要的托管分配或在核心游戏循环区域中占用过多的CPU时间。
您需要首先收集GC.Alloc标记的调用堆栈。如果您对这个过程不熟悉,可以在Unity游戏分析的终极指南的“定位应用程序生命周期中的重复内存分配”部分找到一些技巧和窍门。
如果报告的调用堆栈不够详细,无法追踪分配或其他减速的来源,则可以在启用深度分析的情况下进行第二次分析,以找到分配的来源。
在收集关于帧时间“罪犯”的笔记时,请务必注意它们与帧的其余部分的比较。这种相对影响将受到启用深度分析的影响。
阅读有关深度剖析的更多信息 终极剖析Unity游戏指南。
尽早剖析
剖析带来的最佳收益是在项目开发生命周期的早期开始时获得的。
尽早且频繁地进行剖析,以便您和您的团队理解并记住项目的“性能特征”。如果性能急剧下降,您将能够轻松发现问题发生的时刻并解决问题。
最准确的剖析结果总是来自于在目标设备上运行和剖析构建,同时利用特定平台的工具深入挖掘每个平台的硬件特性。这种组合将为您提供跨所有目标设备的应用性能的整体视图。

识别性能问题
下载此图表的可打印PDF版本 在这里。
在某些平台上,确定您的应用程序是CPU绑定还是GPU绑定很简单。例如,在Xcode中运行iOS游戏时,fps面板显示一个条形图,显示总CPU和GPU时间,以便您可以看到哪个更高。CPU时间包括等待VSync的时间,而VSync在移动设备上始终启用。
然而,在某些平台上获取GPU计时数据可能会很具挑战性。幸运的是,Unity Profiler显示了足够的信息,以识别性能瓶颈的位置。上面的流程图说明了初始剖析过程,后面的部分提供了每个步骤的详细信息。它们还展示了来自真实Unity项目的Profiler捕获,以说明需要关注的内容。
要全面了解所有CPU活动,包括何时等待GPU,请在Profiler的CPU模块中使用 时间线视图。熟悉 常见的Profiler标记 以正确解读捕获。某些Profiler标记可能会根据您的目标平台而有所不同,因此请花时间在每个目标平台上探索您游戏的捕获,以了解您项目的“正常”捕获是什么样的。
项目的性能受限于耗时最长的芯片和/或线程。这就是您应该集中优化工作的领域。例如,想象一个目标帧时间预算为33.33毫秒并启用VSync的游戏:
- 如果CPU帧时间(不包括VSync)为25毫秒,GPU时间为20毫秒,那就没问题!你受限于CPU,但一切都在预算之内,优化不会提高帧率(除非你将CPU和GPU都降到16.66毫秒以下,并提升到60 fps)。
- 如果CPU帧时间为40毫秒,GPU为20毫秒,你受限于CPU,需要优化CPU性能。优化GPU性能无济于事;实际上,你可能想将一些CPU工作转移到GPU上,例如,通过使用计算着色器而不是C#代码来平衡一些工作。
- 如果CPU帧时间为20毫秒,GPU为40毫秒,你受限于GPU,需要优化GPU工作。
- 如果CPU和GPU都在40毫秒,你受限于两者,需要将两者都优化到33.33毫秒以下,以达到30 fps。
请查看这些资源,进一步探讨CPU或GPU受限的情况:

您保持在帧预算之内吗?
在开发过程中尽早并经常对项目进行分析和优化,将帮助你确保所有应用程序的CPU线程和整体GPU帧时间都在帧预算内。
上面是一个来自Unity移动游戏的Profiler捕获图像,该游戏由一个持续进行分析和优化的团队开发。该游戏在高规格手机上目标为60 fps,在中低规格手机上目标为30 fps,例如此捕获中的手机。
注意,所选帧的几乎一半时间被黄色的WaitForTargetfps Profiler标记占用。应用程序已将Application.targetFrameRate设置为30 fps,并启用了VSync。主线程上的实际处理工作在大约19毫秒时完成,其余时间用于等待33.33毫秒的剩余时间过去,然后开始下一帧。尽管这个时间用Profiler标记表示,但主CPU线程在此期间基本上处于空闲状态,允许CPU冷却并使用最少的电池电量。
在其他平台上或如果禁用VSync,需注意的标记可能会有所不同。重要的是检查主线程是否在你的帧预算内运行,或者是否正好在你的帧预算上,并有某种标记指示应用程序在等待VSync,以及其他线程是否有任何空闲时间。
空闲时间由灰色或黄色的Profiler标记表示。上面的截图显示渲染线程在 Gfx.WaitForGfxCommandsFromMainThread 中空闲,这表明它在一帧中完成了向 GPU 发送绘制调用,并在等待 CPU 在下一帧中发出更多绘制调用请求。同样,尽管 Job Worker 0 线程在 Canvas.GeometryJob 中花费了一些时间,但大部分时间它是空闲的。这些都是应用程序在帧预算内舒适运行的迹象。
如果您的游戏在帧预算内
如果您在帧预算内,包括为电池使用和热限制而对预算所做的任何调整,您已经完成了性能分析,直到下次 - 恭喜。考虑运行 内存分析器 以确保应用程序也在其内存预算内。
上面的图像显示游戏在 ~22 毫秒的帧预算内舒适运行,这对于 30 fps 是必需的。注意 WaitForTargetfps 在 VSync 之前填充主线程时间,以及渲染线程和工作线程中的灰色空闲时间。还要注意,通过查看 Gfx.Present 帧的结束时间,可以观察到 VBlank 间隔,并且您可以在时间轴区域或顶部的时间标尺上绘制时间刻度,以测量从一个到下一个。

CPU受限
如果您的游戏不在 CPU 帧预算内,下一步是调查 CPU 的哪个部分是瓶颈 - 换句话说,哪个线程最忙。分析的目的是识别瓶颈作为优化的目标;如果依赖猜测,您可能会优化游戏中不是瓶颈的部分,导致整体性能几乎没有改善。一些“优化”甚至可能会恶化您游戏的整体性能。
整个 CPU 工作负载成为瓶颈的情况很少见。现代 CPU 有多个不同的核心,能够独立且同时执行工作。不同的线程可以在每个 CPU 核心上运行。完整的 Unity 应用程序使用一系列线程用于不同的目的,但最常见的用于查找性能问题的线程是:
- 主线程:这是所有游戏逻辑/脚本默认执行工作的地方,也是物理、动画、UI 和渲染等功能和系统花费大部分时间的地方。
- 渲染线程:在渲染过程中,主线程检查场景并执行相机剔除、深度排序和绘制调用批处理,从而生成要渲染的物体列表。该列表被传递给渲染线程,该线程将其从Unity的内部平台无关表示转换为特定的图形API调用,以指示特定平台上的GPU。
- 工作线程:开发者可以利用C#作业系统来调度某些类型的工作在工作线程上运行,从而减少主线程的工作负载。Unity的一些系统和功能也利用作业系统,例如物理、动画和渲染。
主线程
上面的图像显示了在主线程约束下项目的样子。该项目在Meta Quest 2上运行,通常目标帧预算为13.88毫秒(72 fps)或甚至8.33毫秒(120 fps),因为高帧率对于避免VR设备中的运动病很重要。然而,即使这个游戏的目标是30 fps,很明显这个项目有问题。
尽管渲染线程和工作线程看起来与在帧预算内的示例相似,但主线程在整个帧期间显然忙于工作。即使考虑到帧结束时Profiler的少量开销,主线程仍然忙于超过45毫秒,这意味着该项目的帧率低于22 fps。没有标记显示主线程在等待VSync时闲置;它在整个帧期间都很忙。
下一阶段的调查是识别帧中耗时最长的部分,并理解原因。在这一帧中,PostLateUpdate.FinishFrameRendering耗时16.23毫秒,超过了整个帧预算。更仔细的检查显示有五个名为Inl_RenderCameraStack的标记,表明有五个相机处于活动状态并渲染场景。由于Unity中的每个相机都会调用整个渲染管线,包括剔除、排序和批处理,因此该项目的最高优先级任务是减少活动相机的数量,理想情况下只保留一个。
BehaviourUpdate,包含所有MonoBehaviour Update()方法的标记,耗时7.27毫秒,时间线中的品红色部分表示脚本分配托管堆内存的位置。切换到层级视图并通过在搜索栏中输入GC.Alloc进行过滤,显示在这一帧中分配此内存大约耗时0.33毫秒。然而,这并不是内存分配对CPU性能影响的准确测量。
GC.Alloc标记实际上并不是通过测量从开始到结束的时间来计时的。为了保持其开销小,它们仅记录其开始时间戳以及分配的大小。分析器为它们分配了最少的时间,以确保它们可见。实际的分配可能需要更长时间,特别是当需要从系统请求新的内存范围时。为了更清楚地看到影响,请在执行分配的代码周围放置分析器标记,在深度分析中,时间线视图中洋红色的GC.Alloc样本之间的间隙提供了一些指示,说明它们可能花费了多长时间。
此外,分配新内存可能对性能产生负面影响,这些影响更难以测量并直接归因于它们:
- 从系统请求新内存可能会影响移动设备的功耗预算,这可能导致系统减慢CPU或GPU的速度。
- 新内存可能需要加载到CPU的L1缓存中,从而推送现有的缓存行。
- 增量或同步垃圾收集可能会直接或延迟触发,因为托管内存中现有的空闲空间最终会被超出。
在帧开始时,四个Physics.FixedUpdate实例总计为4.57毫秒。稍后,LateBehaviourUpdate(对MonoBehaviour.LateUpdate()的调用)耗时4毫秒,动画器大约占1毫秒。
为了确保该项目达到其预期的帧预算和速率,所有这些主线程问题都需要调查以找到合适的优化。通过优化耗时最长的事情,可以获得最大的性能提升。
以下领域通常是寻找优化的丰硕之地,尤其是在主线程受限的项目中:
- 物理
- MonoBehaviour脚本更新
- 垃圾分配和/或收集
- 相机剔除和渲染
- 糟糕的绘制调用批处理
- UI更新、布局和重建
- 动画
根据您想要调查的问题,其他工具也可能会有所帮助:
- 对于耗时较长但未能准确显示原因的MonoBehaviour脚本,请在代码中添加分析器标记或尝试深度分析以查看完整的调用堆栈。
- 对于分配托管内存的脚本,启用分配调用堆栈以查看分配的确切来源。或者,启用深度分析或使用项目审计器,它显示按内存过滤的代码问题,以便您可以识别所有导致托管分配的代码行。
- 使用帧调试器调查绘制调用批处理不佳的原因。
有关优化游戏的全面提示,请免费下载这些Unity专家指南:

CPU受限:渲染线程
上面的屏幕截图是一个受其渲染线程限制的项目。这是一个具有等距视角的控制台游戏,目标帧预算为33.33毫秒。
分析器捕获显示,在当前帧开始渲染之前,主线程等待渲染线程,如Gfx.WaitForPresentOnGfxThread标记所示。渲染线程仍在提交来自上一帧的绘制调用命令,并且尚未准备好接受来自主线程的新绘制调用;渲染线程在Camera.Render中花费时间。
您可以区分与当前帧相关的标记和来自其他帧的标记,因为后者看起来更暗。您还可以看到,一旦主线程能够继续并开始发出绘制调用供渲染线程处理,渲染线程需要超过100毫秒来处理当前帧,这在下一帧期间也会造成瓶颈。
进一步调查显示,这款游戏有一个复杂的渲染设置,涉及九个不同的相机和许多由于替换着色器而导致的额外通道。该游戏还使用前向渲染路径渲染超过130个点光源,这可能为每个光源增加多个额外的透明绘制调用。总的来说,这些问题结合在一起导致每帧超过3000个绘制调用。
以下是需要调查的常见原因,适用于受渲染线程限制的项目:
- 绘制调用批处理不佳,特别是在较旧的图形API(如OpenGL或DirectX 11)上。
- 相机数量过多。除非您正在制作分屏多人游戏,否则您应该只拥有一个活动相机。
- 剔除不良,导致绘制的内容过多。调查您的相机的视锥体尺寸和剔除层掩码。考虑启用遮挡剔除。也许甚至可以根据您对世界中物体布局的了解,创建自己的简单遮挡剔除系统。查看场景中有多少个投影阴影的物体——阴影剔除是在与“常规”剔除不同的单独通道中进行的。
渲染分析器模块 显示每帧绘制调用批次和 SetPass 调用的数量概述。调查您的渲染线程向 GPU 发出的绘制调用批次的最佳工具是 帧调试器。

CPU受限:工作线程
被其他 CPU 线程(而非主线程或渲染线程)绑定的项目并不常见。然而,如果您的项目使用 数据导向技术栈 (DOTS),尤其是如果工作被移出主线程到使用 C# 作业系统 的工作线程中,则可能会出现这种情况。
上面的捕获来自编辑器中的播放模式,显示一个在 CPU 上运行粒子流体模拟的 DOTS 项目。
乍一看,这似乎是一个成功。工作线程紧密打包了 Burst 编译的作业,表明大量工作已被移出主线程。通常,这是一个明智的决定。
然而,在这种情况下,主线程的帧时间为 48.14 毫秒,灰色的 WaitForJobGroupID 标记为 35.57 毫秒,表明一切并不顺利。WaitForJobGroupID 表示主线程已安排作业在工作线程上异步运行,但它需要在工作线程完成运行之前获得这些作业的结果。在 WaitForJobGroupID 下方的蓝色分析器标记显示主线程在等待时运行作业,试图确保作业更快完成。
尽管作业是 Burst 编译的,但它们仍在做大量工作。也许这个项目使用的空间查询结构应该被优化或替换为更高效的结构,以快速找到彼此接近的粒子。或者,空间查询作业可以安排在帧的末尾而不是开始,结果在下一帧开始之前不需要。也许这个项目试图模拟太多粒子。需要进一步分析作业的代码以找到解决方案,因此添加更细粒度的分析器标记可以帮助识别它们最慢的部分。
您项目中的工作可能没有像这个例子中那样并行化。也许您只有一个长时间运行的工作在单个工作线程中。这没问题,只要工作被调度的时间和需要完成的时间之间的间隔足够长,以便工作能够运行。如果不是,您将看到主线程停滞,因为它在等待工作完成,如上面的截图所示。
同步点和工作线程瓶颈的常见原因包括:
- 工作未被Burst编译器编译
- 在单个工作线程上运行的长时间工作,而不是在多个工作线程之间并行化
- 在帧中调度工作时与需要结果时之间的时间不足
- 帧中的多个“同步点”,要求所有工作立即完成
您可以使用流程事件功能在CPU使用分析器模块的时间线视图中调查工作何时被调度以及主线程何时期望其结果。有关编写高效DOTS代码的更多信息,请参见DOTS最佳实践指南。

GPU受限
如果主线程在Profiler标记中花费大量时间,例如Gfx.WaitForPresentOnGfxThread,并且您的渲染线程同时显示标记,例如Gfx.PresentFrame或.WaitForLastPresent.,则您的应用程序是GPU绑定的。
以下捕获是在使用Vulkan图形API的三星Galaxy S7上进行的。尽管在这个例子中花费在Gfx.PresentFrame上的一些时间可能与等待VSync有关,但这个Profiler标记的极长时间表明大部分时间花费在等待GPU完成渲染前一帧。
在这个游戏中,某些游戏事件触发了一个着色器的使用,该着色器使GPU渲染的绘制调用数量增加了三倍。在分析GPU性能时需要调查的常见问题包括:
- 昂贵的全屏后处理效果,包括常见的罪魁祸首,如环境光遮蔽和辉光
- 由于以下原因导致的昂贵片段着色器:
- 分支逻辑
- 使用全浮点精度而不是半精度
- 过度使用寄存器会影响GPU的波前占用率
- 透明渲染队列中的过度绘制是由于低效的UI、粒子系统或后处理效果造成的
- 过高的屏幕分辨率,例如在4K显示器或移动设备的Retina显示器上发现的分辨率
- 由于密集的网格几何体或缺乏LOD而导致的微三角形,这在移动GPU上是一个特别的问题,但也会影响PC和控制台GPU
- 由于未压缩的纹理或没有mipmap的高分辨率纹理而导致的缓存未命中和浪费的GPU内存带宽
- 几何体或细分着色器,如果启用了动态阴影,可能会每帧运行多次
如果您的应用程序似乎受限于GPU,您可以使用帧调试器快速了解发送到GPU的绘制调用批次。但是,这个工具无法提供任何具体的GPU时间信息,只能展示整体场景的构建方式。
调查GPU瓶颈原因的最佳方法是检查来自合适GPU分析器的GPU捕获。您使用的工具取决于目标硬件和所选择的图形API。
