UE5 内存盘点与分析

内存盘点分析
VMmap工具
LLM
Memreport -full
Memory Insights
内存预算
总结

概述

本文主要分享内存分析的思路,和PC端的内存分析方法,及相关工具的使用。本文的大致思路如下:

  • 理清楚虚拟内存地址和物理内存的关系。
  • 理清楚进程的虚拟地址空间下的内存的分布情况。
  • 进程的虚拟内存和物理内存盘点,了解程序的大致内存占用情况。
    • 跟着物理内存占用过高的部分,深入分析,寻找分配堆栈。
  • 工具介绍,活用工具。

内存概念

进程虚拟内存:操作系统分配给进程的虚拟地址空间,虚拟内存地址会映射到物理内存地址上。

物理内存:就是内存条上的内存,也是我们实际需要优化的内存空间。

显存:显卡上的内存,也属于物理内存,所以进程上分配的显存也会占用虚拟地址空间的。

通俗的解释就是,我们的程序在虚拟地址上进行内存的分配、释放、读写。而这些分配、释放、读写操作在进入操作系统后(从用户态进入内核态),操作系统会把虚拟地址映射到物理内存上(通过页表进行映射),再进行我们相应的调用操作。一个关键信息是,当我们分配一段内存时,操作系统只是分配给了我们一片连续的虚拟地址空间,并没有进行物理内存上的分配,而当我们进行该地址上读写时,触发操作系统的缺页异常,这个时候才会真正的分配物理内存。

早期的计算机只有4G物理内存,实际上是受到了总线的位宽限制,总线位宽32bit,寻址能力只能达到4GB(2^32)。随着用户的需求不断增加,计算机硬件的也在不断发展,现在市面上的计算机总线位宽基本都是64bit,甚至包括移动平台设备。当寻址能力达到2^64 =?byte时,我们的物理内存大小理论上几乎可以随意扩展。

虚拟内存到物理内存映射

然后对于操作系统,物理内存是可以直接进行读写操作的,但在操作系统创建新的用户进程时,操作系统并不想免用户进程直接操作物理内存,因为这将带来风险和内存管理成本提升。比如用户进程可以通过地址寻址操作跨进程读取和修改内存数据。当增加这些物理内存的归属标记时,就增加了管理成本,也无法对内存碎片进行优化。虚拟地址作为内存的寻址中间态应运而生。

 

详细资料:《内存管理》 《内核虚拟地址空间》

进程虚拟内存地址分布概览

当进程被创建时,进程中的地址访问都是基于虚拟地址的,包括代码段、数据段、堆栈区。

代码段

一个可执行程序,在硬盘上是二进制序列,当CPU开始执行他时,需要先把这些可执行指令代码加载到内存中,包括各类加载的动态链接库,这一片区域将被称为代码段,占用一部分虚拟地址空间。

数据段

程序在启动时,在main函数入口之前,会先初始化各种静态数据,这些数据存在全局数据区,占用一部分虚拟地址空间。

堆栈区

栈区:包括主线程、子线程的函数调用中在临时分配的内存存放区,例如:临时变量,函数参数。

堆区:通过new 、malloc等函数进行分配的内存。new函数会直接调用VirtualAlloc来分配虚拟内存。而malloc会调用heapalloc,但heapalloc低底层还是VirtualAlloc,只是操作系统加一层包装,便于管理C函数分配。

 

详情请看 :这篇讲的内核区分布《Kernel Virtual Address Layout》,用户区的分布可以参考LInux的文章。

内存盘点分析

内存分析之前,我们得先做一次内存盘点,摸清楚我们的游戏进程所占虚拟内存和物理内存的分布情况。然后着重的根据各个大块中,物理内存占用的情况来进行优化。为什么选择物理内存高的地方呢?因为虚拟内存的占用其实有很多一部分空间中是闲置的,并不能实际的反应到我们的内存占用。实际上只是地址空间,

VMmap工具

之所以介绍这个工具,是因为代码段和静态数据段的内存占用也是内存优化的一部分,而这部分内存在我们的程序中的内存管理器中追踪不到,所以需要外部工具进行追踪,我们也要对其进行初步盘点,甚至包括堆内存也可以辅助UE中的LLM统一分析。

VMmap

Image:可执行文件和动态链接库的内存占用,一般是代码段的数据。

Mapped File :内存映射文件,一般是就是想磁盘文件的内存映射。

Shareable : 共享内存占用。

Heap:属于堆内存的一部分,主要是C函数的分配(malloc/realloc/free,HeapAlloc进行分配的内存),这里应该是没有包括C++的new操作符分配的堆(VirtualAlloc函数分配),虽然实际上HeapAlloc的还是会走VirtualAlloc,但是我们可以通过这个确定应用程序中通过C函数进行的内存分配,这些占用主要是存在第三方库中。

Managed heap:属于堆内存的一部分,托管堆内存,.net的托管内存。

Stack: 栈内存,每个线程都会分配一块栈内存空间,用于栈上变量、函数参数的内存分配。windows一般默认一个线程的1MB占空间,根据函数调用栈的深度物理内存会逐渐升高。UE中是有默认值设置的,可以参考官方文档。

Private Data:私有数据,总堆内存,一般是程序内进行分配的内存。

Page Table:页表的内存占用,一般是虚拟内存到物理内存的映射关系表。

 

有两个很关键的信息决定我们优化的方向,虚拟内存和物理内存的占比,当一段区域的物理空间占用较高时将是我们优化的重点。一般情况下都是堆内存的物理占用很高,因为基本都是游戏内的资源数据的分配,需要通过程序内的工具进行解析优化(下节会提到)。也可以通过Heap的物理占用情况来排除第三方库对物理内存造成的影响,例如日志、堆栈信息、debug信息都会占用较高的物理内存。

 

分配函数调用堆栈

想要看到堆栈信息,必须要通过命令行启动进程。每次分配的调用堆栈,虽然会很碎片化,但是也可以了解大概的内存分配的情况。如果需要获得该堆栈下内存分配的统计信息可能需要更专业和定制化的工具。

VMMap堆栈信息

 

小结

到这里,我们基本是盘点了整个应用程序的内存。针对物理内存的占用情况着重的去分析和优化。当然VMMap的作用主要是辅助了解应用程序的内存状况,需要更多的细节,我们可能需要一些定制化的工具能够对分配堆栈进行统计来观察各个模块上的分配情况,也可以通过时序上的差异来确定内存泄露的地方。

LLM

回到UE中来,就是对程序内的堆堆内存进行分析。UE中的low Level Memeory会统计UE中所有能被追踪到的内存占用,大部分为堆内存。UE会为每个module重载new/delete操作符,通过这个来管理UE中所有模块的内存分配和释放,如下的宏进行声明。

//模块的实现宏示例
IMPLEMENT_APPLICATION(SlateViewer, "SlateViewer");

//模块宏,内置了很多UE的公共头文件
#define IMPLEMENT_APPLICATION

//模块前缀宏
#define PER_MODULE_BOILERPLATE 

//new/delete操作符重载宏。
#define REPLACEMENT_OPERATOR_NEW_AND_DELETE 

启用LLM 只需要再启动命令行加一个“-llm”参数,“stat llm”和“stat llmfull”指令可以观察当前的虚拟内存占用信息。

 

FMemory

在重载new操作符后,所有的分配都经过FMemory进行,而FMemory中的在不同的平台会使用不同的内存分配进行管理,如下列出了UE5中各种分配器类型。

	/** Which allocator is being used */
	enum EMemoryAllocatorToUse
	{
		Ansi, // Default C allocator
		Stomp, // Allocator to check for memory stomping
		TBB, // Thread Building Blocks malloc
		Jemalloc, // Linux/FreeBSD malloc
		Binned, // Older binned malloc
		Binned2, // Newer binned malloc
		Binned3, // Newer VM-based binned malloc, 64 bit only
		Platform, // Custom platform specific allocator
		Mimalloc, // mimalloc
	};
  • Ansi :标准C内存分配器,直接调用malloc、free、realloc函数。
  • Stomp:用于排查非法的内存操作(如:内存越界访问,野指针),目前只支持windows、mac、unix等pc平台。带命令行参数-stompmalloc来启用该分配器。
  • TBB:(Thread Building Blocks)内存分配器,Intel 提供的第三方库的一个可伸缩内存分配器(Scalable Memory Allocator),目前没看到哪里用,。
  • Jemalloc内:存分配器(Linux / FreeBSD)适合多线程下的内存分配管理 canonware.com/jemalloc/
  • Mimalloc:编辑器下默认的分配器类型 github.com/microsoft/mi
  • Binned: (第一代箱式内存分配器)
  • Binned2:(第二代箱式内存分配器)
  • Binned3:(第三代箱式内存分配器,仅支持64bits)

 

我们不讨论每个分配器版本的实现原理,只要知道他们在向操作系统申请内存时是使用VirtualAlloc还是HeapAlloc(C函数malloc,虽然最终还是VirtualAlloc),区别在于C函数堆会有自己的堆内存管理,UE中追踪这部分内存分配,要比向操作系统申请(VirtualAlloc)的分配虚拟内存要小。

回到LLM

VirtualAlloc的分配,实际上我们都是可以通过LLM追踪到的,盘点和泄漏问题都可以跟进去分析。但是LLM中有一张UnTracked部分是无法追踪的,他的计算公式是: untracked=total−tracktotaluntracked = total - tracktotal ,其中total是直接通过操作系统取到的该进程所占的虚拟内存大小,tracktotal部分既是LLM追踪到的所有虚拟内存。那untrack部分就是包括第三方动态库的内存分配、显存地址空间。所以这部分内存需要用前面提到的VMMap通过堆栈来确定分配位置,和通过物理内存的占用来评估是否值得优化,因为这部分内存往往要优化起来未知因素比较多,比如第三方库是否包含源码。

LlmFull

Memreport -full

这个CVar指令可以把UE中当前的帧的所有内存资源导出,包括所有的UObject、分类收集的Component、以及RHI资源、如果开了LLM,还会把LLM一起导出。

 

内存总览

进程内存状况

RHI内存(GPU显存)

RHI资源总览,GPU内存占用:

RHI资源

紧接着会把前50名的高内存占用的资源列出来,当然我们也可以通过修改指令把所有的资源打印出来:

RHI资源列表

对于Non-Transient资源,主要来源是美术资源和渲染管线中需要跨帧用到的RT或者Buffer。而Transient资源,一般是在渲染管线的一帧中某一阶段使用的GPU资源,比如Gbuffer中的normal RT在光照阶段后就不会使用了,这块内存可以映射成其他的资源供其他阶段使用。

RHI资源

再紧接着,渲染管线上的资源命名一般都带有一些关键词,UE会跟进这些关键词进行分类来展示渲染管线上不同模块的显存占用,也可以很好帮助我们去分析显存的问题,甚至如果我们确定好了各模块的显存预算,可以直接定位问题。

美术资源

剩下的就是美术资源列表了,"Obj List: class=StaticMesh -resourcesizesort"可以打印出所有的静态模型资源的内存信息。

staticmesh 资源内存占用

简单描述一下各列的信息:

  • NumKB:特指该类对象的在内存占用(在创建该类对象时所占用的内存大小,一般不包括从磁盘上加载的数据,因为这些数据是指针的形式存在另一块内存区域。)。
  • MaxKB:特指该类对象的内存占用的最大值,一般和NumKB相等。
  • ResExcKB:资源的内存占用,由磁盘上的数据加载至内存或者显存的总和。
  • ResExcDedSysKB:资源的主存的占用。
  • ResExcDedVidKB:资源在显存的占用。
  • ResExcUnkKB:资源的未知部分,一般根据不同类型的资源,会附加一些特殊的信息,比如模型资源中,附加在顶点上的多层uv或者color。

 

在BaseEngine.ini中可以修改cmd列表来添加打印出来的UObject列表信息:

BaseEngine.ini

 

Memory Insights

Trace Insights是UE内置的性能分析工具,包括CPU、GPU、Net、IO、Memory指标追踪,针对内存的这一部分的指标,实际上都来源于LLM模块,能看到每帧的内存的变化情况,各类Tag, Module的内存情况,不过展示的都是虚拟内存,可能无法准确的定位物理内存的占用,但是从峰值上考虑,肯定是能够降低物理内存的,还有对于急速增加的虚拟内存也可以考虑跟进去分析分析原因的。

Memory Insights

内存预算

我们做内存盘点的目的不仅仅为了了解程序的内存占用情况,还是为了能够更好的确定内存的预算。根据当前的硬件等级进行评估出总的内存预算,然后按照个各个模块逐个去进行分配。为了达到极致的效果,我们一般都会选择用空间换时间的策略,内存的开销也就随之增大。物理内存峰值是我们主要优化的重点,因为在如果峰值如果过高,在一些内存吃紧的设备上会直接crash。

总结

本文并没有提到内存优化的部分,主要分享了内存分析的思路,定位内存问题往往要比优化内存问题更复杂和繁琐,而且分析工具在定位问题上也起到了关键性的作用,UE内部提供的分析工具十分强大,配合自己的业务需求,可以做到日常的性能监控。在实际的开发过程中更多的性能问题是由美术资源引起的,解决这部分问题基本上能解决80%的性能问题。所以制定资源使用规范也是相当有必要的,既能在前期就避免问题的出现,还能减少程序排查问题的精力。

posted on 2025-03-01 18:16  zxddesk  阅读(478)  评论(0)    收藏  举报

导航