Unity 中的内存分析
内存分析有助于测试硬件平台的内存限制,减少加载时间和崩溃,并使项目与旧设备兼容。如果你想通过做出实际上会增加内存使用量的更改来提高 CPU/GPU 性能,这也可能与此有关。这在很大程度上与运行性能无关。
有两种方法可以分析 Unity 应用程序中的内存使用情况。
内存剖析器模块这是一个内置的剖析器模块,可为您提供有关应用程序内存使用情况的基本信息。
内存分析器软件包这是一个 Unity 软件包,您可以将其添加到您的项目中。它为 Unity 编辑器添加了一个额外的内存分析器窗口,您可以利用它更详细地分析应用程序中的内存使用情况。可以存储和比较快照以便查出内存泄漏,或者查看内存布局以查出内存碎片问题。
利用这些内置工具,你可以监控内存使用情况,查找应用程序中内存使用率高于预期的区域,并发现和改善内存碎片。
了解目标设备的内存限制并做好预算对多平台开发至关重要。在设计场景和关卡时,请遵守为每个目标设备设定的内存预算。通过设置限制和准则,可以确保您的应用程序在每个平台的硬件规格范围内运行良好。
您可以在开发人员文档中找到设备内存规格。例如,根据文档显示,Xbox One 游戏机前台运行游戏的最大可用内存限制为 5 GB。
它还可用于设置网格和着色器复杂度的内容预算,以及纹理压缩。这些都会影响内存的分配。在项目开发周期中可以参考这些预算数字。
确定物理 RAM 限制
每个目标平台都有内存限制,了解了内存限制后,就可以为应用程序设置内存预算。使用内存剖析器查看捕获快照。硬件资源(见上图)显示物理随机存取内存(RAM)和视频随机存取内存(VRAM)的大小。这个数字并没有考虑到并非所有空间都可以使用的情况。不过,它为开始工作提供了一个有用的大致数字。
最好交叉参考目标平台的硬件规格,因为这里显示的数字不一定总能反映全貌。开发人员工具包硬件有时会有更多内存,或者您可能正在使用具有统一内存架构的硬件。
为所支持的每个平台确定内存规格最低的硬件,并以此指导内存预算决策。请记住,并非所有的物理内存都可以使用。例如,控制台可以运行管理程序来支持旧版游戏,这可能会占用总内存的一部分。考虑使用一个百分比(如总数的 80%)。对于移动平台,您还可以考虑将其分为多个规格级别,以便为高端设备用户提供更好的质量和功能。
一旦确定了内存预算,就可以考虑为每个团队设置内存预算。例如,环境美工人员在加载每个关卡或场景时都会获得一定量的内存,音频团队会为音乐和音效分配内存,等等。
随着项目的进展,对预算采取灵活的态度非常重要。如果一个团队的预算远远低于预算,那么就把盈余分配给另一个团队,只要它能改善他们正在开发的游戏领域。
一旦决定并设定了目标平台的内存预算,下一步就是使用剖析工具来帮助您监控和跟踪游戏中的内存使用情况。
内存剖析器模块提供两种视图:简单而详细。使用 "简单 "视图可获得应用程序内存使用情况的高级视图。必要时,切换到详细视图以进一步深入。
简单
预留内存总量 "即 "统一内存跟踪总量"。它包括 Unity 已预留但现在未使用的内存(该数字为 "已用内存总量")。
系统已用内存 "是操作系统认为您的应用程序正在使用的内存。如果该数字显示为 0,则表明 Profiler 计数器未在您正在进行剖析的平台上运行。在这种情况下,最好的指标是总预留内存。在这种情况下,还建议改用本地平台剖析工具来获取详细的内存信息。
要了解可执行文件、动态链接库和 Mono 虚拟机使用了多少内存,逐帧内存数据是不够的。使用 "详细快照捕获 "来挖掘这种内存故障。
注意:Memory Profiler 模块 "详细 "视图中的引用树仅显示本地引用。从UnityEngine.Object继承而来的对象类型的引用可能会显示其托管 shell 的名称。不过,它们可能只是因为下面有本地对象才会显示出来。您不一定会看到任何管理类型。举例来说,一个对象的某个字段中包含一个 Texture2D 作为引用。使用该视图时,也看不到哪个字段持有该引用。要了解此类细节,请使用内存剖析器软件包。
要确定内存使用量何时开始接近平台预算,请使用下面的 "纸巾背面 "计算方法:
系统已用内存(如果系统已用内存显示为 0,则为总预留内存)+未跟踪内存的大概缓冲区/平台总内存
当这一数字开始接近平台内存预算的 100% 时,请使用Memory Profiler 软件包找出原因。
Memory Profiler 软件包取代了 Memory Profiler 模块的许多功能,但您仍然可以使用该模块来补充您的内存分析工作。
例如:
- 发现 GC 分配:虽然这些问题会在模块中显示出来,但使用Project Auditor或 Deep Profiling 更容易追踪到它们。
- 快速查看堆的已用/保留大小
- 着色器内存分析
在设置内存预算时,请记住在整个目标平台上配置规格最低的设备。密切监控内存使用情况,牢记目标限制。
您通常希望使用功能强大、内存容量大的开发系统进行配置文件(存储大容量内存快照或快速加载和保存这些快照的空间非常重要)。
内存剖析与 CPU 和 GPU 剖析不同,它本身会产生额外的内存开销。您可能需要在高端设备(拥有更多内存)上配置内存,但要特别注意低端目标规格的内存预算限制。
分析内存使用情况时的注意事项
- 在功能更强大的设备上,质量级别、图形层级和 AssetBundle 变体等设置可能会有不同的内存使用量。例如:
- 质量级别和图形设置可能会影响用于阴影贴图的 RenderTextures 的大小。
- 分辨率缩放可能会影响屏幕缓冲区、渲染纹理和后期处理效果的大小。
- 纹理质量设置会影响所有纹理的大小。
- 最大 LOD 可能会影响模型等。
- 如果您有 AssetBundle 变体,如高清(HD)和标清(SD)版本,并根据设备规格选择使用哪一个,那么您也可能会根据正在进行剖析的设备获得不同的资产大小。
- 目标设备的屏幕分辨率会影响用于后期处理效果的渲染纹理的大小。
- 设备支持的图形 API 可能会影响着色器的大小,这取决于 API 是否支持着色器的变体。
- 分层系统可使用不同的质量设置、图形层级设置和资产包变化,是针对更多设备的好方法,例如,在 4 GB 移动设备上加载高清版本的资产包,在 2 GB 设备上加载标清版本的资产包。不过,请注意上述内存使用量的变化,并确保测试两种类型的设备,以及具有不同屏幕分辨率或支持图形 API 的设备。
注意:由于从编辑器和 Profiler 中加载了额外的对象,Unity 编辑器通常会显示较大的内存占用空间。它甚至可能显示在构建过程中不会加载到内存中的资产内存,例如来自资产包(取决于可寻址模拟模式)或精灵和地图集的资产内存,或检查器中显示的资产内存。编辑器中的一些参考链可能也会比较混乱。
内存剖析器目前是 Unity 2019 LTS 或更新版本的预览版,但预计将在 Unity 2022 LTS 中得到验证。
Memory Profiler 软件包的一大优势是,除了捕获本地对象(如 Memory Profiler 模块所做的)外,它还允许你查看托管内存、保存和比较快照,并通过可视化的内存使用明细,更详细地探索内存内容。
快照显示了引擎中的内存分配情况,让你可以快速找出内存使用过多或不必要的原因,追踪内存泄漏,或查看堆碎片。
安装内存分析器软件包后,单击窗口 > 分析 > 内存分析器打开它。
通过 Memory Profiler 的顶部菜单栏,可以更改播放器选择目标,并捕捉或导入快照。
注意:通过 "目标选择 "下拉菜单将内存分析仪连接到远程设备,对目标硬件上的内存进行分析。在 Unity 编辑器中进行剖析时,由于编辑器和其他工具会增加开销,因此得出的数据并不准确。
Memory Profiler 窗口左侧是工作台区域。用它来管理、打开或关闭已保存的内存快照。您还可以使用此区域在 "单个快照 "和 "比较快照 "视图之间进行切换。
与 Profile Analyzer 类似,Memory Profiler 允许加载两个数据集(内存快照)进行比较。这在查看内存使用量随时间或场景之间的增长情况以及搜索内存泄漏时特别有用。
Memory Profiler 主窗口中有多个选项卡,可让你深入查看内存快照,包括摘要、对象和分配以及碎片。让我们逐一详细了解这些选项。
如果想快速了解项目的内存使用情况,请选择此视图。它还包含与捕获的内存快照相关的有用和重要的内存数据。它非常适合快速浏览快照拍摄时的情况。
树形图 "视图以图形化的 "树形图"方式显示对象使用的内存明细,您可以深入其中,发现消耗内存最多的对象类型。
树形图 "视图下方是一个过滤表,可更新显示所选网格单元格中的对象列表。
树形图显示归属于本地或托管对象的内存。托管对象内存与本地对象内存相比往往相形见绌,因此在地图视图中很难发现。您可以放大树形图来查看这些内容,但要查看较小的对象,表格通常能提供更好的概览。单击树形图中的单元格,就可以根据部分的类型过滤下面的表格,和/或选择表格中感兴趣的特定对象。
通过选择表格行或树形图网格单元格,然后选中详细信息侧面板中的 "引用 "部分,就可以追踪到哪些项目引用了该列表中的对象,以及这些引用所在的托管类字段。如果侧边是隐藏的,可以通过窗口右上角工具栏中的切换按钮使其可见。
注意:树形图只显示内存中的对象。这并不能完全代表跟踪记忆。如果您发现 "内存使用情况概览 "中的数字与 "跟踪内存 "中的总数不一致,请务必了解这一点。
这是因为并非所有本地内存都与对象绑定。它还可以由非对象关联的本地分配组成,例如可执行文件和 DLL、NativeArray 等。即使是更抽象的概念,如 "保留但未使用的内存空间",也会影响本地分配总数。
对象和分配 "视图显示一个表格,可以根据现成的选择项(如所有对象、所有本地对象、所有托管对象、所有本地分配等)切换筛选。
您可以切换底部表格,以显示所选范围内的对象、分配或内存区域。如树形图视图所述,并非所有内存都与对象相关联,因此 "所有内存区域 "和 "所有本地分配 "页面可以提供更全面的内存使用情况,其中 "内存区域 "还包括保留但当前未使用的内存。
在优化内存使用时,可以利用这一点,在内存预算有限的硬件平台上更有效地打包内存。
加载内存剖析器快照,并通过树形图视图检查类别,按内存占用大小从大到小排列。
项目资产往往是内存消耗最大的部分。使用表格视图查找纹理对象、网格、音频剪辑、渲染纹理、着色器和预分配缓冲区。这些都是内存优化的好对象。
内存泄漏通常发生在以下情况
- 不能通过代码手动从内存中释放对象
- 由于无意中的引用,一个对象留在了内存中
Memory Profiler Compare 模式可通过比较特定时间范围内的两个快照,帮助查找内存泄漏。
Unity 游戏中常见的内存泄漏情况可能发生在卸载场景之后。
Memory Profiler 软件包有一个工作流程,可指导您使用比较模式发现这些类型的泄漏。
通过对多个内存快照进行差异化比较,可以确定应用程序生命周期内持续内存分配的来源。
以下各节列出了一些提示,可帮助识别项目中的托管堆分配。
Unity Profiler 中的 Memory Profiler 模块用红线表示每帧的托管分配。在大多数情况下,该值应为 0,因此,如果该行出现任何峰值,则表明您应调查管理分配的帧。
CPU 使用情况剖析器模块中的时间轴视图会以粉红色显示分配情况,包括受管分配情况,便于查看和调整。
CPU 使用情况剖析器中的 "层次 "视图可让您单击列标题,将其用作排序标准。按 GC Alloc 排序是关注这些数据的好方法。
Project Auditor是一款实验性静态分析工具。它可以做很多有用的事情,其中有几件超出了本指南的范围,但它可以生成一个列表,列出项目中导致托管分配的每一行代码,而无需运行该项目。这是一种非常有效的发现和调查此类问题的方法。
Unity 使用Boehm-Demers-Weiser 垃圾收集器,它会停止运行你的程序代码,只有在工作完成后才恢复正常执行。
注意不必要的堆分配会导致 GC 峰值。
- 弦乐在 C# 中,字符串是引用类型,而不是值类型。这意味着每个新字符串都将在托管堆上分配,即使它只是临时使用。减少不必要的字符串创建或操作。避免解析 JSON 和 XML 等基于字符串的数据文件,而是以 ScriptableObjects 或 MessagePack 或 Protobuf 等格式存储数据。如果需要在运行时构建字符串,请使用StringBuilder类。
- 调用 Unity 功能:一些 Unity API 函数会创建堆分配,尤其是那些返回托管对象数组的函数。缓存对数组的引用,而不是在循环中间分配它们。此外,还要利用某些避免产生垃圾的功能。例如,使用GameObject.CompareTag代替手动将字符串与GameObject.tag 进行比较(因为返回新字符串会产生垃圾)。
- 拳击避免用值类型变量代替引用类型变量。这样就创建了一个临时对象,随之而来的潜在垃圾会隐式地将值类型转换为对象类型(例如,int i = 123; object o = i)。取而代之的是,尽量提供具体的重载,并使用您想要传递的值类型。这些重载也可以使用泛型。
- Coroutines:虽然 yield 不会产生垃圾,但创建一个新的 WaitForSeconds 对象却会产生垃圾。缓存并重复使用 WaitForSeconds 对象,而不是在 yield 行中创建或使用 yield return null。
- LINQ 和正则表达式这两种情况都会产生幕后拳击垃圾。如果性能是个问题,请避免使用 LINQ 和正则表达式。编写 for 循环并使用列表来替代创建新数组。
- 通用集合和其他托管类型:不要在 Update 中的每一帧都声明并填充列表或集合(例如,玩家一定半径范围内的敌人列表)。取而代之的是,将 List 作为 MonoBehaviour 的成员,并在 Start 中对其进行初始化。只需在使用前清空每一帧的收藏即可。
尽可能缩短垃圾回收时间
如果您确定垃圾回收冻结不会影响游戏中的某个特定点,您可以使用System.GC.Collect.
请参阅 "了解自动内存管理",了解如何利用这一优势的示例。
使用增量式垃圾回收器分担 GC 工作量
增量式垃圾回收不是在程序执行过程中创建一个单一的、长时间的中断,而是使用多个较短的中断,将工作量分散到多个帧中。如果垃圾回收导致帧频不稳定,请尝试使用该选项,看看能否减少 GC 峰值问题。使用配置文件分析器来验证其对应用的益处。
请注意,在增量模式下使用 GC 会为某些 C# 调用添加读写障碍,这也会带来一些开销,每帧脚本调用开销最多可增加 ~1 毫秒。为了获得最佳性能,最理想的做法是在主要游戏循环中不使用 GC Allocs,这样就不需要增量 GC 来获得流畅的帧速率,并且可以将 GC.Collect 隐藏在用户不会注意到的地方,例如在打开菜单或加载新关卡时。
要了解有关内存剖析器的更多信息,请查看以下资源:
- 内存剖析器文档
- 使用 Unity 中的内存分析器提高内存使用率教程
- 内存性能分析器用于排除内存相关问题的工具 Unite 会话
- 使用内存剖析器Unity 学习课程