技术博客

内存分析:MemReport 与 LLM 标签对账

第 97 / 100 篇 · 2025-12-20 · ue5 · 性能优化内存LLM

"游戏吃了 8GB 内存"是个没法行动的数字。内存问题要分成三层看:系统层(进程实际占了多少)、引擎层(各系统认领了多少)、分配器层(内部碎片浪费了多少)。三层数字永远对不齐,但对账的过程本身就是定位的过程。这一篇讲 UE5.3 的对账工具和一条实战排查路径。

三层账本

  • 系统层:任务管理器 / Activity Monitor 看的 RSS。它包含引擎数据、分配器缓存、GPU 驱动映射、第三方库的一切 malloc,是唯一"真实"的数字,但也是最没结构的;
  • 引擎层:LLM(Low Level Memory Tracker)按标签记账。LLM_SCOPE(Stat_XXX) 在代码里标作用域,LLM_PLATFORM_SCOPE 标平台分配,运行期 -llm 开启,结果落 CSV(Saved/Profiling/LLM)或控制台 stat llm 实时看。标签体系把 RSS 拆成 Engine、Rendering、Physics、Audio、UObject 这些桶;
  • 分配器层:UE 默认 MallocBinned2/3,按尺寸分级池化。记账单位是池粒度,申请 100 字节可能占 128 字节的池槽,差额就是内部碎片。stat memory 和 MemReport 里的 allocator 段能看到池统计。

对账恒等式可以写成:

$$\text{RSS} = \sum \text{LLM 标签} + \text{Untagged} + \text{分配器内部碎片} + \text{驱动/外部映射}$$

三项"解释不了的"里,Untagged 是可控的(该打标签没打),碎片是半可控的(换分配器或调池参数),驱动映射基本不可控(显存映射、IO 缓存)。排查顺序从可控项开始。

Untagged 桶:标签缺失的信号

stat llm 里 Untagged 占比高(经验上超过两三成就该查)说明有大量分配没走 LLM_SCOPE。两个常见来源:第三方库自己 malloc(Lua、物理中间件),以及项目代码里直接 FMemory::Malloc / new 没包作用域。治理手段是在模块初始化处加 LLM_SCOPE,第三方库要么接受它算 Untagged,要么在其分配回调里挂标签。Untagged 降不下去时,对账就永远差一块,后续的所有判断都带着不确定性。

MemReport 实战流程

控制台 MemReport -full 会输出一份全量报告:平台内存概况、分配器池统计、按类统计的 UObject 数量和体积、纹理/声音/动画各自的预算占用。留档做 diff 是主要用法,经典流程:

  1. 游戏跑到稳态,执行 MemReport -full,存为 baseline;
  2. 反复执行可疑操作 N 次(比如反复进出某个关卡、反复打开关闭 UI);
  3. 再出一份 MemReport,和 baseline 逐段 diff。

判断标准:做 N 次操作后,UObject 某类的实例数应该回到基线值。回不去就是泄漏特征——要么对象没被 GC 根解除(还挂在某个容器的强引用里),要么资源型对象(纹理、动画)的缓存没清。报告里按类列出的数量和体积就是嫌疑清单,配合 obj list class=XXX 看实例都挂在谁的引用下。

配合 GC 命令把干扰去掉:两次快照之间手动 gc.CollectGarbage,把"只是还没轮到 GC"的对象和"永远 GC 不掉"的对象分开。后者才是真泄漏。

三层对账:RSS = LLM 标签 + Untagged + 碎片 + 驱动映射 RSS(系统视角,唯一真实数字) LLM 标签 Engine / Rendering Physics / Audio ... Untagged 第三方库 / 漏标 占比高就要补标签 分配器碎片 池粒度取整的差额 MallocBinned2/3 MemReport diff 流程(泄漏定位) 稳态快照 → 重复可疑操作 N 次 → 手动 GC → 再快照 → 按类对比实例数 实例数回不到基线 = 泄漏;obj list class=XXX 追引用挂在哪

专项:纹理与流送

Rendering 标签下的大头常年是纹理。纹理有两个独立账本要分开看:CPU 侧的解压缩/驻留内存(LLM Rendering 的一部分)和 纹理流送池(r.Streaming.PoolSize,GPU 侧预算,超了会降 mip 导致糊)。stat streaming 看流送状态:池占用率、强制流送的纹理数。池长期顶满不是"内存泄漏",是内容超预算—— mip 尾数、纹理组预算(DeviceProfiles 的 TextureLODGroups)和流送目标精度(r.Streaming.MipBias)是三个常规调节旋钮。

常见误区

  • 把 RSS 当指标优化:分配器会缓存 freed 内存不立刻还给系统,RSS 高位不等于泄漏,看趋势不看绝对值;
  • 只看总量不分类:MemReport 的价值全在分类段,总量 diff 对不上时先怀疑驱动映射和第三方库,不是引擎泄漏;
  • 快照不手动 GC:不 GC 的 diff 里混着"还没到 GC 时机"的噪声,手动 GC 一次再对比,嫌疑名单直接短一半。

内存对账的核心心法:永远拿同一把尺子(LLM 标签 / MemReport 分类段)量同一个时刻(稳态 + 手动 GC),三次以上的测量趋势才构成结论。