找回密码
 立即注册
查看: 445|回复: 0

UE4 性能 - (一)瓶颈定位

[复制链接]
发表于 2024-8-2 09:28 | 显示全部楼层 |阅读模式
前言


  • 接触 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:可将所有正在 tickactor 打印到 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) 组成的像素块
        • 当模型存在较多狭长、细小的三角形时,有效面积较小,但可能占用了很多 QuadQuad 被多次反复绘制,会导致大量像素参与到无意义的计算中,引起不必要的性能开销









        • 进入 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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-21 22:35 , Processed in 0.101634 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表