UE4 性能 - (一)瓶颈定位
前言[*]接触 UE4 开发已经接近两年,记得转向 UE4 引擎后的第一个任务,就是在开发初期先对 UE4 的性能优化方面做一些探索,以便半途有必要进行性能优化时能够当即上手;前期的探究与后来的实践共同证明,性能优化是一项广度与深度并存、值得反复研究的复杂工作。在终于下定决心开始系统性撰写技术文章的此时,不妨从这个话题切入,结合期间读到的各类文章、视频以及个人在开发过程中的有限经验,依次从 UE4 的性能瓶颈定位、性能分析和优化方式 逐步展开对 UE4 性能优化问题的探讨。
[*]为了优化阅读体验,避免篇幅过长,本篇只着重介绍瓶颈定位的部门,性能分析与性能优化部门将另开新篇进行展开。
[*]P.S. 对于某个具体问题,我个人方向于遵循 WHY → WHAT → HOW 的思考方式(重要性逐级递减) 加以理解。因为如果找不到做某件事情的意义(WHY)地址,或是对这件事情本身的定义(WHAT)都模棱两可,那么即便颠末大量实践(HOW),常识体系也难以成型,且一旦相应的工作被不成避免地中断,已经掌握的常识也容易被迅速遗忘、难以回溯。
WHY
stat fps
[*]首先,感知上我必定是感觉这个游戏很“卡”,才会萌生要搞清楚它 “为什么卡” 的问题,这个 “卡” 的感觉,可以用 UE4 里最常用的命令之一来描述:stat fps
显示 fps 与当前帧的总耗时
stat unit
[*]其次,之所以需要花精力去“定位”,是由于造成卡顿的原因有多种,而在 UE4 体系中,造成卡顿的因素大致分为三类,隐含在另一个最常用的命令之中:stat unit
[*]Frame: 即一帧所耗费的总时间,这个值越大,fps 就越小,二者相乘恒等于 1
[*]Game: 措置游戏逻辑所耗费的时间
这一步完全不考虑衬着问题,表示的是整个游戏世界在一帧之内,只在逻辑层面措置所有的变化需要花多长时间——Compute Game Context
[*]Draw: 筹备好所有必要的衬着所需的信息,并把它从 CPU 发送给 GPU 所耗费的时间
承接上一步,在游戏世界在逻辑层完成所有的计算和模拟后,收集衬着所需的信息,并剔除非必要信息,通知 GPU 进行画面衬着—— What to Render
[*]GPU: 接收到衬着所需信息之后,将像素最终的表示画在屏幕上的耗时
WHAT
Overview
[*]瓶颈定位,就是要找到造成性能开销的最大元凶,也就是确定优化的基本标的目的,才能深入和落实到细节层面,进行后续的分析和优化工作。而要搞清楚开销主要发生在哪个阶段,不成避免地还是要对 Game, Draw, GPU 对应的三类线程以及它们之间的关系有更详细的认识
Game Thread, Draw Thread, GPU Thread 的关系
[*]stat unit 显示的数值,都是在 一帧之内 的耗时,那么在这一帧期间,Game, Draw, GPU 这三者是如何先后执行的,可以参照下图加以理解
[*]Game Thread 首先会对整个游戏世界进行逻辑层面的计算与模拟(e.g. Spawn 多少个新的 actor、每个 actor 在这一帧位于何处、角色移动、动画状态等等),所有这些信息会被输送到 Draw Thread
[*]Draw Thread(也叫 Rendering Thread) 会按照这些信息,剔除(Culling)掉不需要显示的部门(e.g. 处于屏幕外的物体),接着创建一个列表,此中包含了衬着每个物体必备的关键信息(e.g. 如何被着色、映射哪些纹理等等),再将这个列表输送给 GPU Thread
[*]GPU Thread 在获取了这个列表之后,会计算出每个像素最终需要如何被衬着在屏幕上,形成这一帧的画面
[*]综上,对于每一帧来说,这三者的执行挨次依次为:Game Thread → Draw Thread → GPU Thread
Notes
[*]一帧的总耗时,取决于三者中开销最严重、即耗时最长的线程
[*]Game Thread 和 Draw Thread 在 CPU 上运行,GPU Thread 在 GPU 上运行
[*]如果 GPU Thread 率先完成了它的工作,而其他二者仍在工作中(e.g. 已经绘制好了当前帧,但下一帧的数据还没拿到),那么 GPU 就会等待 CPU 的指令而导致下一帧的画面姗姗来迟;反之如果 GPU 耗时更严重,导致 CPU 输送的数据没有被及时措置,使得画面没能被及时衬着,同样会导致卡顿
HOW
Overview
[*]要定位开销发生在哪个线程,最直接的方式是按照 stat unit 给出的信息,斗劲 Game, Draw, GPU 三者哪一个与 Frame 的数值最接近(如上述所说,一帧的总耗时取决于三者中的最大值),则它就是造成开销的主要因素
[*]操作 UE 内部丰硕、强大的各种命令,还可定位出开销具体发生在哪个线程的哪个阶段
[*]同时善用 控制变量法,对判断加以验证
Game Thread
[*]Game Thread 造成的开销,基本可以归因于 C++ 和蓝图的逻辑措置,瓶颈常见于Tick 和代价昂贵的逻辑实现(Expensive Functionality)
[*]Tick
[*]大量物体同时 Tick 会严重影响 Game Thread 的耗时
[*]stat game:显示 Tick 的耗时情况
[*]dumpticks:可将所有正在 tick 的 actor 打印到 log 中
[*]复杂逻辑
[*]需要借助 Unreal Frontend Profiler / Unreal Insights 等东西对游戏逻辑中开销较大的代码进行定位,后续将详细说明它们的使用方式
Draw Thread (Rendering Thread)
[*]Draw Thread 的主要开销来源于 Visibility Culling 和 Draw Call
[*]Visibility Culling
[*]Visibility Culling 会基于深度缓存(Depth Buffer) 信息,剔除位于相机的视锥体(Frustum)之外的物体和被遮盖住(Occluded)的物体,当游戏世界中可见的物体过多,剔除所需的计算量也将变大,导致耗时过长
[*]stat initviews:显示 Visibility Culling 的耗时情况,同时还能显示当前场景中可见的 Static Mesh 的数量(Visible Static Mesh Elements)
[*]Draw Call
[*]一般理解:CPU 筹备好一系列衬着所需的信息,通知 GPU 进行一次衬着的过程
[*]想象 CPU 指挥 GPU 拿起一支笔刷,蘸好颜料,给某个(或者某一些)多边形(polygon)涂上颜色,来自 CPU 的这条指令就是一次 Draw Call
[*]很多情况下,分歧的多边形(可能属于分歧的 mesh)需要的是同一种颜色(材质),那么在给笔刷蘸好颜色之后,可以一次性给这些多边形上色,而不需要做无谓的反复操作,这个过程就叫做 合批(batching)
[*]UE 官方解释:a group of polygons sharing the same material (一组使用不异材质的多边形)
[*]这个解释虽然准确,但乍一看非常抽象。首先举例来理解:场景中有 100 个多边形(polygon),此中 10 个共同使用材质 A,10个共同使用材质 B,残剩 80 个共同使用材质 C,100 个多边形被分成了 3 组,于是 Draw Call 就等于 3
[*]结合之前的一般理解,也可以理解为:CPU 命令 GPU 将笔刷蘸上某一材质对应的颜料,然后一次性给若干个 polygon 上色,这条 CPU 下达的指令就是一次 Draw Call,而这些 polygon 就是 one group of polygons sharing the same material,有多少组这样的 polygon,就等于发生了多少次 Draw Call
[*]stat SceneRendering 可查看 Mesh Draw Call 的数量
[*]即便场景中模型面数多,只要合批机制完善,Draw Call 的数量也可以非常少
[*]对比于面数,Draw Call 对性能开销的影响要大得多
GPU Thread
[*]顶点措置(Vertex-bound) 导致的瓶颈
[*]Dynamic Shadow
[*]目前动态暗影(Dynamic Shadow)的生成主要依赖 Shadow Mapping,一种在光栅化阶段计算暗影的技术,Shadow Mapping 每生成一次暗影需要进行两次光栅化,因此当顶点数过多(可能源于多边形数量巨大,也可能源于不适当的曲面细分) 时,Dynamic Shadow 将成为 GPU 在光栅化阶段的一大性能瓶颈
[*]ShowFlag.DynamicShadows 0: 使用该指令可封锁场景内的动态暗影(0暗示封锁,1暗示开启),可在开启和封锁两种状态间反复切换,查看卡顿情况是否发生明显变化,以此判断 Dynamic Shadow 是否确实造成了巨大开销
[*]着色(Pixel-bound) 导致的瓶颈
[*]运行指令 r.ScreenPercentage 50,暗示将衬着的像素数量减半(也可替换成其他 0-100 之间的数),不雅察看卡顿现象是否明显减缓,以此判断瓶颈是否 Pixel-bound
[*]Shader Complexity
[*]显示对每一个像素所执行的着色指令数量,数量越多,消耗越大
[*]场景中存在过多的半透明物体(Translucent Object),会显著增加 Pixel Shader 的计算压力,使用 stat SceneRendering 可查看 Translucency 的消耗情况;使用 ShowFlag.Translucency 0 来封锁(0暗示封锁,1暗示开启)所有半透明效果
[*]当着色器(材质连线)的实现逻辑过于复杂或低效时,也会导致较高的 Shader Complexity
[*]在 Viewport 中选择 Optimization Viewmodes → Shader Complexity,可视化 Shader 造成的开销
[*]Quad Overdraw
[*]着色期间 GPU 的大部门操作不是基于单个像素,而是一块一块地绘制,这个块就叫 Quad,是由 4 个像素 (2 × 2) 组成的像素块
[*]当模型存在较多狭长、细小的三角形时,有效面积较小,但可能占用了很多 Quad,Quad 被多次反复绘制,会导致大量像素参与到无意义的计算中,引起不必要的性能开销
[*]进入 Optimization Viewmodes → Quad Overdraw,显示 GPU 对每个 Quad 的绘制次数
[*]Light Complexity
[*]场景内的动态光源(Dynamic Lights) 数量过多时,会发生大量动态暗影(Dynamic Shadow),如上述所说,容易引起较大开销
[*]动态光源的半径过大,导致多个光源的范围呈现大量交叠,也可能导致严重的 Overdraw 问题
[*]进入 Optimization Viewmodes → Light Complexity,查看灯光引起的性能开销
[*]内存(Memory-bound)引起的瓶颈
[*]有时性能瓶颈还在于过高的内存占用,此中最常见的是大量的纹理(Texture)加载和采样
[*]使用 stat streaming overview,查看当前纹理对内存的占用情况
[*]对于纹理的优化,后续将另开新篇加以详细介绍
参考
Unreal Art Optimization
Profiling and Optimization in UE4 | Unreal Indie Dev Days 2019 | Unreal Engine
UE4 Graphics Profiling: Measuring Performance
UE4 Graphics Profiling: Pipeline and Bottlenecks
Performance Tools in Unreal Engine
Shadow Casting
Performance and Profiling Overview
Ray Tracing Features Settings
Dynamic Scene Shadows
Ray Tracing in UE4
Stat Commands
页:
[1]