性能优化问题的本质
慢与快的问题
前提
稳定性:不能因优化造成稳定性变差
兼容性:不能因优化导致兼容性变差
性价比:优化要有度,考虑成本与复杂度
性能优化的流程
- 发现问题(什么平台、什么操作系统、什么情况下出现问题,一般问题还是特例问题等)
- 定位问题(什么地方造成的性能问题,我们要用什么工具、什么方法确定瓶颈)
- 研究问题(确定用什么方案处理这个问题,要考虑性能优化的前提)
- 解决问题(按问题研究的结论去实际处理,并验证处理结果与预期的一致性)
影响性能的问题
CPU
通常,CPU 渲染时间的最大贡献者是向 GPU 发送渲染命令的成本。渲染命令包括绘制调用(绘制几何形状的命令),以及在绘制几何形状之前更改 GPU 上设置的命令
减少 Unity 渲染的对象数量
- 考虑减少场景中对象的总数,例如:使用 skybox 来创建遥远几何的效果
- 执行更严格的剔除,以便 Unity 绘制更少的对象。考虑使用遮挡剔除来防止 Unity 绘制隐藏在其他物体后面的物体,减少相机的远夹平面,以便更远的物体落在其果实之外,或者,对于更细粒度的方法,将物体放入单独的层,并使用 Camera.layerCullDistances 设置每层剔除距离
减少 Unity 渲染每个对象的次数
- 在适当的情况下,使用 light mapping 来烘焙(预计算)照明和阴影。这增加了构建时间、运行时内存使用和存储空间,但可以提高运行时性能
- 如果应用程序使用 Forward rendering,请减少影响对象的每像素实时灯光的数量
- 实时阴影可能是非常资源密集型,因此请谨慎而高效地使用它们
- 如果应用程序使用反射探针,请确保您优化其使用
GPU
填充率的限制
GPU 试图每帧绘制的像素比它所能处理的要多,如果是这种情况,请考虑以下选项:
- 识别并减少应用程序中的透支。透支最常见的贡献者是重叠的透明元素,如 UI、粒子和 Sprite,在 Unity 编辑器中,使用 Overdraw Draw模式 来识别有问题的区域
- 降低片段着色器的执行成本
- 如果您使用的是 Unity 的内置着色器,请从 Mobile 或 Unlit 类别中选择一个。它们也适用于非移动平台,但它们是更复杂的着色器的简化和近似版本
- 动态分辨率是一个 Unity 功能,允许您动态缩放单个渲染目标
内存带宽的限制
GPU 正在尝试向其专用内存读取和写入比它在帧中可以处理的更多数据。这通常意味着有太多的纹理,或者纹理太大。如果是这种情况,请考虑以下选项:
- 为与相机距离在运行时变化的纹理启用 mipmap(例如,3D场景中使用的大多数纹理),这增加了这些纹理的内存使用量和存储空间,但可以提高运行时 GPU 的性能
- 使用合适的压缩格式来减小内存中纹理的大小。这可能会导致更快的加载时间、更小的内存占用和更高的 GPU 渲染性能。压缩纹理仅使用未压缩纹理所需的内存带宽的一小部分
顶点处理的限制
这意味着GPU正在尝试处理比它在帧中处理的更多的顶点。如果是这种情况,请考虑以下选项:
- 降低顶点着色器的执行成本
- 优化您的几何形状:不要使用不必要的三角形,并尽量保持紫外线映射接缝和硬边(加倍顶点)的数量
- 使用 Level of Detail 系统
降低渲染频率
有时,降低渲染帧速率可能会使应用程序受益,这不会降低渲染单个帧的 CPU 或 GPU 成本,但它减少了 Unity 这样做的频率,而不会影响其他操作(如脚本执行)的频率
可以降低应用程序部分或整个应用程序的渲染帧速率。降低渲染帧速率,以防止不必要的电力使用,延长电池寿命,并防止设备温度上升到 CPU 频率可能受到限制的程度。这在手持设备上特别有用
隐藏的几类小问题
- 功耗比
- 填充率
- 发热量
性能问题可能的情况
- 瓶颈可能性按由高到低的顺序排列(UP 经验总结)
- CPU 利用率
- 带宽利用率
- CPU / GPU 强制同步
- 片元着色器指令
- 几何图形到 CPU 到 GPU 的传输
- 纹理 CPU 到 GPU 的传输
- 顶点着色器指令
- 几何图形复杂性
经常用的优化思路
- 升维与降维
升维:优化性能,但算法不易理解 降维:算法容易理解,但性能优化差
- 维度转换,如空间与时间、量纲转换
{%note primary%}
AoS and SoA
结构数组(AoS),数组结构(SoA)或数组结构数组(AoSoA)是排列内存中记录序列的对比方法,涉及交错,并且对 SIMD 和 SIMT 编程有关
标准 C# 数组是 AoS,但 SoA 的结构适用于使用 CPU 缓存,CPU 缓存比主内存快,以及 SIMD 的超快速并行处理
AoS
面向对象思想,不同字段的数据在其中交错,这通常更直观,并由大多数编程语言直接支持
-----------------------------------------------------------------------------
| double | int | char | *pad* | double | int | char | *pad* | double | int | char |
-----------------------------------------------------------------------------
SoA
面向数据思想,将记录的元素(或 C 编程语言中的“结构”)分离成每个字段一个并行数组。在大多数指令集架构中,使用打包的 SIMD 指令更容易操作,因为单个 SIMD 寄存器可以加载同质数据,可能由广泛的内部数据路径(例如128位)传输
-----------------------------------------------------------------------------
| double | double | double | *pad* | int | int | int | *pad* | char | char | char |
-----------------------------------------------------------------------------
如果只需要记录的特定部分,则只需要迭代这些部分,允许将更多数据放入单个缓存行中。缺点是遍历数据时需要更多的缓存方式,以及低效的索引寻址
差异
由于每个对象都保存在一起,因此 AoS 对程序员来说更容易阅读
如果结构的所有成员一起访问,AoS 可能会有更好的缓存位置
SoA 可能更有效率,因为将相同的数据类型组合在一起有时会暴露矢量化
在许多情况下,SoA 使用的内存较少,因为填充仅在数组之间,而不是在每个结构之间
StructureOfArraysGenerator
Structure of arrays source generator 插件制造 CPU 缓存和 SIMD 友好的数据结构制造高性能代码用于 .NET 和 Unity 平台
点击链接以了解更多
{%endnote%}