2278 字
11 分钟
UE 性能分析——用 Stat 与 Unreal Insights 找到那一帧

一、性能问题不是“感觉有点卡”#

60 FPS 的目标意味着每帧只有约 16.67ms:

16.67ms 帧预算
├── Game Thread → Gameplay、Actor Tick、复制准备
├── Render Thread → 生成渲染命令
├── RHI Thread → 提交图形 API 命令
└── GPU → 真正执行绘制、计算、后处理

但这些不是简单相加。CPU 和 GPU 可能并行工作,真正决定帧率的是当前流水线中最慢的环节:

Game Thread 8ms → Render Thread 12ms → GPU 20ms
GPU 是瓶颈

当 GPU 变快后,Game Thread 可能又成为瓶颈。所以“我把某个函数优化了 2ms”只有在它确实位于关键路径上时才有意义。

性能分析的第一步永远是测量瓶颈,而不是猜。


二、先建立帧预算#

对一个 60 FPS 的多人合作游戏,可以先设一个粗略预算:

区域目标预算主要内容
Game Thread6–8ms回合逻辑、UI 状态、Actor Tick
Render Thread4–6ms渲染命令、场景代理更新
GPU10–14ms阴影、材质、后处理、UI
网络处理包含在 CPU 内收包、复制、RPC、序列化
余量2–4ms突发事件、平台差异

这不是必须遵守的固定数字,目标是让团队有一个共同语言:

“这项功能大概需要 0.3ms Game Thread”
“这个功能应该挺轻的”
更适合做工程决策。

30 FPS、120 FPS、掌机和移动端会有不同预算。性能目标应该在项目早期声明,而不是上线前才第一次测量。


三、Stat 命令:先做现场体检#

UE 的 Stat 命令适合快速回答“哪一类东西在花时间”。常用命令包括:

stat unit // Game、Draw、RHIT、GPU 等帧时间
stat fps // 帧率和基础帧时间
stat game // Actor Tick、组件和 Gameplay 相关统计
stat sceneRendering
stat gpu // GPU Pass 时间(平台支持时)
stat slate // Slate / UI 统计
stat net // 网络收发、带宽、复制相关数据

不同引擎版本、平台和渲染后端显示的字段可能不同,重点是先看量级和趋势,不要把某个统计项的名字当成绝对真相。

1 stat unit 怎么看#

Frame: 16.6 ms
Game: 7.2 ms
Draw: 10.8 ms
GPU: 14.4 ms

这里的 Frame 受到同步关系影响,不一定等于三者相加。判断瓶颈时,看 Game、Draw、GPU 谁接近或超过自己的预算,再用更细的工具下钻。


四、Unreal Insights:从一帧下钻到函数#

Stat 告诉你“哪一层慢”,Unreal Insights 用时间线告诉你“慢在哪里”。它适合分析:

  • 长帧和随机 Hitch
  • Game Thread 上突然出现的函数尖峰
  • 线程之间的等待关系
  • 异步加载、GC、Shader 编译造成的卡顿
  • 网络收包和复制任务的时间分布

一个常见的分析流程:

1. 复现问题并记录 Trace
2. 在 Insights 中找到长帧
3. 观察 Game / Render / RHI / GPU 时间线
4. 放大长帧,找到最长的 Scope
5. 回到代码确认调用路径
6. 修复后用相同场景重新记录

1 Bookmark 比“看起来卡”有用#

TRACE_BOOKMARK(TEXT("MissionFinished"));

在回合结束、生成大量物品、切换关卡等关键位置打 Bookmark,之后能在时间线上快速定位业务事件。没有书签时,你只能在一堆匿名的函数调用里猜哪一帧是问题发生的。

2 自定义计时 Scope#

TRACE_CPUPROFILER_EVENT_SCOPE(MissionSettlement);
ResolveWinner();
ApplyRewards();
SaveRoundResult();

Scope 名称要表达业务动作,而不是重复函数名。一个好的 Scope 让你可以直接回答“结算到底花在选胜者、发奖励还是存档”。


五、Game Thread 常见瓶颈#

1 Tick 数量不是唯一指标#

1000 个很轻的 Tick 可能不如一个做了全表扫描的 Tick:

void UQuestSubsystem::Tick(float DeltaSeconds) {
for (const FPlayerData& Player : AllPlayers) {
for (const FItemData& Item : AllItems) {
Evaluate(Player, Item);
}
}
}

如果玩家和物品都增长,这段逻辑是 O(P × I)。优化方向可能是:

  • 只在物品状态变化时重新计算
  • 缓存不变的估价结果
  • 把筛选拆成索引或 Tag 集合
  • 将不影响本帧结果的工作延迟到任务队列

2 蓝图也需要被测量#

蓝图不是天然慢,问题通常是:

  • Event Tick 里反复查找对象
  • 每帧创建临时数组或字符串
  • 嵌套循环扫描所有 Actor
  • UI Binding 隐式高频求值
  • 大量 Cast 和组件查找

先用 Profiler 找到实际 Scope,再决定是否搬到 C++。把没有瓶颈的蓝图全改成 C++,很可能只增加维护成本。


六、GPU 瓶颈:不要只看 Draw Call 数量#

GPU 时间可能来自:

几何处理 → 阴影 → Base Pass → 光照 → 反射 / GI
→ 后处理 → TSR / TAA → UI

常见方向:

现象可能原因
分辨率提高后时间近似按像素增长像素着色器、后处理、带宽
远处物体增加后变慢几何、阴影、可见性
只有大量透明特效时变慢半透明 Overdraw
阴影质量提高后明显变慢Shadow Map / Virtual Shadow Maps
UI 很多但场景不变时变慢Slate 绘制、布局失效或过度绘制

Draw Call 数量只是线索。一个复杂材质的 Draw Call 可能比多个简单材质更贵;一个透明粒子还可能重复覆盖同一组屏幕像素很多次。


七、Hitch:平均帧率掩盖不了长帧#

平均 60 FPS 不代表体验稳定:

大多数帧:16ms
偶发一帧:250ms
平均值仍然可能接近 60 FPS
玩家却明显感觉到卡顿

所以要同时看:

  • P50:典型帧
  • P95 / P99:尾部帧时间
  • 最大帧时间:最严重的卡顿
  • Hitch 发生时的业务事件

常见 Hitch 来源:

LoadSynchronous()
Shader 编译
大规模 UObject 创建
GC / Cluster 重建
同步保存
首次创建渲染资源

解决思路不是“把一切放到后台线程”。某些 UObject、渲染资源和 Gameplay 状态只能在特定线程或阶段操作。正确做法是使用引擎提供的异步加载、分帧创建、预热和资源生命周期机制。


八、网络性能也要放进同一张图#

多人游戏的性能不只是一条 FPS 曲线:

客户端帧率
↔ Game Thread 收包 / 处理复制
↔ 发送输入 / RPC
↔ 服务器 Tick / 复制准备
↔ 带宽、丢包、重传

服务器每秒向所有连接检查所有 Actor,会同时消耗 CPU 和带宽。前面讲的 Relevancy、Dormancy、Replication Graph,最终都应该通过测量验证:

优化前:每个客户端 120KB/s,复制准备 4ms
优化后:每个客户端 38KB/s,复制准备 1.2ms

不要只看客户端“收到了多少字节”,还要看服务器为生成这些字节做了多少工作。


九、一个可重复的性能分析流程#

定义目标
→ 复现并记录基线
→ Stat 确认瓶颈线程
→ Insights 定位业务 Scope
→ 提出一个最小改动
→ 在相同场景重新测量
→ 记录收益、代价和回归风险

一次只验证一个主要假设:

假设:回合结算卡顿来自同步保存
实验:只把保存改成异步,其他代码不动
结果:长帧从 180ms 降到 32ms
结论:继续处理异步保存完成前的生命周期问题

如果一次改动同时重写了数据结构、Tick 和 UI,很难知道收益来自哪里,也很难在之后发生回归时定位原因。


十、合作副本的性能检查表#

回合开始
→ 是否一次性 Spawn 太多 Actor?
→ 物品图标和预览模型是否同步加载?
战斗阶段
→ UI 是否每帧轮询 MissionState?
→ 每次技能请求是否全量扫描玩家和目标?
→ 战斗事件是否广播了不必要的数据?
回合结束
→ 结算、奖励、统计是否在同一帧完成?
→ 存档是否阻塞 Game Thread?
→ 结果 UI 是否触发整棵 Widget Tree 重排?

性能不是某个“优化阶段”的独立任务。它和资产加载、复制、消息、UI、存档都有连接,应该在每一篇架构设计里顺手问一句:这个状态变化的频率和拥有者是什么?


十一、总结#

概念一句话
帧预算把 16.67ms 或目标平台的预算分给各个系统
Stat快速确认 Game、Draw、GPU、网络等大方向
Unreal Insights用时间线从长帧下钻到业务 Scope
Hitch关注 P95/P99 和最大帧时间,而不只是平均 FPS
CPU 瓶颈常见于 Tick、全量扫描、同步加载和对象创建
GPU 瓶颈常见于像素、阴影、透明 Overdraw、后处理和带宽

性能优化不是找到一个“慢函数”然后结束,而是建立一套可重复的测量—假设—验证循环。 当每一次优化都有基线和证据,性能就不再是玄学,也不会只依赖某个同事的体感。

UE 性能分析——用 Stat 与 Unreal Insights 找到那一帧
https://www.m4doka.xyz/posts/ue/ue-14-performance-insights/
作者
m4doka
发布于
2026-08-28
许可协议
CC BY-NC-SA 4.0