UE4内存优化

UE4内存优化

https://zhuanlan.zhihu.com/p/1087487843

移动平台内存参考

分析工具

ADB

adb shell dumpsys meminfo http://com.xxx.xxx 或者pid

衡量指标:PSS/Private Dirty

  1. PSS (Proportional Set Size) = 私有内存占用 + 共享内存占用 / 共享进程数
  2. Private Dirty 值是仅分配给您的应用堆的实际 RAM
  3. Rss Total =私有内存占用 + 共享内存占用
  4. Native 分配的堆内存占用大小
  5. Dalvik Heap 虚拟机堆内存占用大小
  6. Dalvik 虚拟机的 JIT 和 GC 占用的内存
  7. .so mmap native 机器码所占内存
  8. .dex mmap / .jar mmap dex 字节码占用内存和 jar 字节码占用内存

perfetto heapprofile

1. 下载https://raw.githubusercontent.com/google/perfetto/master/tools/heap_profile 保存为heap_profile.py

2. 脚本编辑

setx PERFETTO_BINARY_PATH F:\perfetto\memory\lib #设置符号所在目录
setx PERFETTO_SYMBOLIZER_MODE index
rmdir /s /q output
mkdir output
python3 ./heap_profile.py -n tencent.lolm -c 1000 -o ./output 

3. 将生成的out目录下的symbolized-trace 拖拽到https://ui.perfetto.dev/ 点击采样点进行分析

AndroidStudio

用AS的Profiler查看,能够看到每帧内存使用情况

memreport

命令行里输入memreport -full即可,它会立即生成一份内存快照,存放在ProjectDir/Saved/Profiling/Memreports里以.memreport结尾的文件里

  1. Obj List 当前游戏中的蓝图类及其创造的对象和对应数量
  2. RHI resource memory RHI的Texture,Uniform Buffer,Pixel Buffer,Vertex Buffer
  3. Levels 当前加载的关卡,类似于stat levels指令显示出来的结果
  4. Listing all textures. 当前游戏中用到的贴图信息,包括贴图的大小,格式等等,这块内存的估算还算比较准,并且用来排查一些不合理的、较大的贴图占用也是十分的方便。
  5. ParticleSystems 粒子相关
  6. SkeletalMesh, StaticMesh 骨骼网格体以及静态网格体

LLM(Low Level Memory)

比 memreport 统计数据更加详细精确,实现参考LowLevelMemTracker.cpp

LLM采用的是SCope插桩模式,在需要做内存统计的地方插入LLM_SCOPE(ELLMTag::XXX_TAG)

UE4引擎封装了FMalloc去接管了malloc的内存分配,所以UE4引擎在new一个对象的时候会调用到FMemory::Malloc()函数,通过创建对应平台的内存分配器去进行内存的分配,并通过OnLowLevelAlloc()函数将这部分的内存统计到了LLM系统中去

需要开启宏ALLOW_LOW_LEVEL_MEM_TRACKER_IN_TEST=1

启动添加参数,或者UE4CommandLine.txt中添加

  • -LLM:运行时打开 LLM 统计
  • -LLMCSV:将内存统计信息输出到 CSV 文件中,CSV 文件保存在 Saved\Profiling\LLM 目录下

常见统计分类

  • Total: 总内存
  • ELLMTag::PlatformTotal: 在 Android 上为 RSS,在 iOS 上为 resident_size
  • ELLMTag::Total: PlatformTotal - LLM Overhead,即为上面的内存减去 LLM 本身消耗的内存
  • Untracked: 未知的内存分配,即 LLM 无法追踪的内存
  • ELLMTag::FName
  • ELLMTag::AudioMisc
  • ELLMTag::Networking
  • ELLMTag::Meshes
  • ELLMTag::StaticMesh
  • ELLMTag::SkeletalMesh
  • ELLMTag::InstancedMesh
  • ELLMTag::Stats
  • ELLMTag::Shaders 各种类型 Shander 的内存占用
  • ELLMTag::PSO Pipeline State Object 缓存的内存占用
  • ELLMTag::Textures 纹理相关的内存占用
  • ELLMTag::RenderTargets
  • ELLMTag::Materials: 材质相关的内存,包含 MaterialInterface, MaterialFunction
  • ELLMTag::Particles 特效相关的内存,包含 ParticleSystemComponent
  • ELLMTag::UI: Slate 相关的内存,包含字体,TextureAtlas
  • ELLMTag::PhysX: PhysX 物理相关的内存
  • ELLMTag::LoadMapMisc: 地图加载过程中的内存,分别为以下两个函数
  • ELLMTag::StreamingManager: StreamableManager 相关的内存,资源 streaming 函数执行过程中的内存
  • ELLMTag::GraphicsPlatform: 显存,只在 D3D,Vulkan 上有
  • ELLMTag::AssetRegistry: AssetRegistryModule 内存

LLM的问题

插桩加标记,没有标记到的地方就没办法统计,会被归入到Untracked部分

关于图形API创建用到的部分内存LLM也有做估算,比如Texture以及Buffer部分,但是这部分没有单独分出来统计,不过可以通过stat rhi指令查看

内存优化方案

AssetRegistry

AssetRegistry是Editor的一个子系统,用来收集未加载到内存里的asset信息,当打包Cook完资源后会生成一个assetregistry.bin的文件,包含所有asset的信息,并在启动游戏的时候将这部分信息加载到内存中,游戏资源比较多,内存占用也会比较大,也会影响到FName部分的内存占用。在游戏中不太会用到AssetRegistry的相关接口去查找资源的话,而只需要搜索的目录路径同步进来即可。可通过在DefaultEngine.ini里加入如下配置来省掉这部分内存占用,同时也能缩减包体

[AssetRegistry]
bSerializeAssetRegistry=False

UE4的多语言切换功能会用到AssetRegistry.GetAssetsByPath(),需要通过AssetRegistry.ScanPathsSynchronous()接口将多语言目录同步到AssetRegistry系统中即可

#if WITH_EDITOR
	// Make sure the asset registry has the data we need
	{
		TArray<FString> LocalizedPackagePaths;
		LocalizedPackagePaths.Add(InLocalizedRoot);

		// Set bIsScanningPath to avoid us processing newly added assets from this scan
		TGuardValue<bool> SetIsScanningPath(bIsScanningPath, true);
		AssetRegistry.ScanPathsSynchronous(LocalizedPackagePaths);
	}
#else
	bool bSerializeAssetRegistry = true;
	GConfig->GetBool(TEXT("AssetRegistry"), TEXT("bSerializeAssetRegistry"), bSerializeAssetRegistry, GEngineIni);
	// Make sure the asset registry has the data we need
	if (!bSerializeAssetRegistry)
	{
		TArray<FString> LocalizedPackagePaths;
		LocalizedPackagePaths.Add(InLocalizedRoot);

		// Set bIsScanningPath to avoid us processing newly added assets from this scan
		TGuardValue<bool> SetIsScanningPath(bIsScanningPath, true);
		AssetRegistry.ScanPathsSynchronous(LocalizedPackagePaths);
	}
#endif // WITH_EDITOR

Shader

材质内存的优化思路是尽量减少shader变种,减少shadercode的大小,关掉不必要的Rendering选项,减少动态光源数量等

1. 变体裁剪优化,关闭不必要的rendering选项

减少shader变体组合数量

2. 开启共享材质和库

将大量变体产生的shader有重复的去重,shadercode存入共享库,每个MaterialInstance对象上只存ShaderCode的GUID

3. 低端机discard掉用不到的Material Quality

4. 关闭材质材质Usage减少vertexfactor

OpenGL Shader

UBO

通过glgenbuffer之类的接口创建buffer都存在一个4k左右的基础开销.OpenGL.UseEmulatedUBs=1启用emulated uniform buffers

开启emulated ubo后会将多个uniformbuffer在CommitPackedUniformBuffers函数中合并提交,这样能降低内存的同时也降低了提交次数.

[/Script/Engine.RendererSettings]
OpenGL.UseEmulatedUBs=1

Program LRU

shader这块内存的占用其实可以分为两部分:一个是CPU这块的binary数据,一部分是UseProgram后在GL里加载program后占用的内存。UE4默认是不会清理编译好并且加载到GL里的program。

UE4还是提供了一个Program LRU的功能,通过r.OpenGL.EnableProgramLRUCache开启,可以设置r.OpenGL.ProgramLRUCount以及r.OpenGL.ProgramLRUBinarySize来限制Program最多加载数量以及加载到GL中的最大内存占用. RHICreateBoundShaderState_OnThisThread这个函数有BUG(首次UseProgram时Texture没绑定)高版本已修复

Shader Map

LLM统计得shader内存占用很大部分都是shadermap中shader变体得占用,除了尽量减少shader得变种之外,我们还可以通过设置r.DiscardUnusedQuality将不需要得QualityLevel的shader不加载进来,但是这有一个问题就是不支持在游戏内动态切换QualityLevel。

[/Script/Engine.RendererSettings]
r.DiscardUnusedQuality=True

FileSystem

文件系统相关的内存,主要是文件读取时的缓冲区,比如 Pak 文件。在DefaultEngine.ini加入如下配置来优化这部分的内存占用。

  1. UnloadPakEntryFilenamesIfPossible=true会调用UnloadPakEntryFilenames函数将Pak里的用来检索的Index清理掉,里面创建的Filename字符串也会释放掉,转而用FilenameHashs来代替检索,节省大部分FileSystem的内存占用
  2. ShrinkPakEntriesMemoryUsage开启后,会将Pak里Entries部分的内存进行压缩
  3. DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames是用来排除那些不想UnloadPakEntryFilenames的目录的
[Pak]
UnloadPakEntryFilenamesIfPossible=True
DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames="*/Content/Localization/*"
ShrinkPakEntriesMemoryUsage=True

把FindFilesAtPath函数里的条日志暴露出来后运行游戏就能确定要排除的目录

void FindFilesAtPath(ContainerType& OutFiles, const TCHAR* InPath, bool bIncludeFiles = true, bool bIncludeDirectories = false, bool bRecursive = false) const
{
	if ((Directory.StartsWith(MountPoint)) || (MountPoint.StartsWith(Directory)))
	{
		if (bFilenamesRemoved)
		{
			FPlatformMisc::LowLevelOutputDebugString(*(FString("FindFilesAtPath() used when bFilenamesRemoved == true: ") + InPath));
		}
         }
}

GPU Particles

fx.AllowGPUParticles关掉,引擎会用到两张128位1024的RT存gpu particle的position和velocity,占用60MB的内存大小

Program Size

系统最开始初始化获得的内存使用,被LLM推测为可执行程序即初始dll本身的内存, Android端占用较多。通过开启隐藏符号文件来减少这部分开销的。

Texture

Texture Streaming修改预设值

UE4自己的算法计算出当前应当缓存的miplevel,将所有需要流送的纹理缓存大小相加后如果超出预设值,则会按照纹理优先级大小去降低对应纹理用于缓存的miplevel,直到满足预设的大小为止

优先级规则StreamingTexture.cpp

  • 保留地形纹理、强制加载纹理和已经缺失分辨率的纹理
  • 保留在屏幕上可见的mip
  • 保留角色纹理和不占用过多内存的纹理
  • 删掉不可见的mip,先删掉最新看到的mip
[/Script/Engine.RendererSettings]
+CVars=r.Streaming.PoolSize=30 
+CVars=r.Streaming.MipBias=1 
+CVars=r.Streaming.MaxTempMemoryAllowed=5

Memory Bucket

通过Memory Bucket系统去限制不同机器的Texture可加载的最大miplevel

DefaultEngine.ini

[PlatformMemoryBuckets]
DefaultMemoryBucket_MinGB=3
SmallerMemoryBucket_MinGB=2
SmallestMemoryBucket_MinGB=1

在DefaultDeviceProfiles.ini对想做内存控制的TextureGroup进行设置

+TextureLODGroups=(Group=TEXTUREGROUP_World,LODBias_Smaller=1,LODBias_Smallest=2,...)

Texture Compression

ASTC压缩,UE4里默认的是用6x6的块来进行压缩的,当然我们也可以通过ASTC Compression Quality vs Size设置来对默认压缩块的大小进行调整,这里的0-4分别对应的是12x12,10x10,8x8,6x6,4x4,还可以通过ASTC Compression Quality vs Speed来调整压缩速度

贴图单独设置其Compression Quality

Normal Texture

UE4里默认Normal Texture是使用4x4的块来压缩,不会受到ASTC Compression Quality vs Size的设置影响。可以修改FORCED_NORMAL_MAP_COMPRESSION_SIZE_VALUE值来强制修改。同时修改BASE_ASTC_FORMAT_VERSION触发重新cook或者清理后重新cook

UI Texture

UI贴图也都尽量采用可压缩的贴图格式而不是ui interface这种不可压缩格式,贴图的边长必须为4的倍数才会采用可压缩的格式.

Weak Reference

UI贴图比较大,由于默认情况下贴图资源被CDO引用住无法GC掉,可以用弱引用技术的方式来缓解这个问题。如下先设置Brush的bind函数,而不是直接设置Image,然后将要加载进来的texture设置为软引用,通过LoadTexture动态的加载进来即可。

CookCommand 启用TextureMaxSize

2D贴图最大限制最大大小,尽量为2的整数次幂,贴图都通过 TextureGroup 限制最大大小

TextureCube限制大小256设置压缩HDRCompressed

R11G11B10

默认HDR下我们使用FP16的RGBA,把Depth存到SceneColor的A通道。

r.Mobile.SceneColorFormat来调整成R11G11B10减少带宽的占用,可能就会使得某些依赖读回深度的feature发生问题。需要设备支持DepthStencil fetch扩展解决问题,或者使用MRT写深度

mobilecontentscalefactor设置backbuffer

减少带宽,r.ScreenPercentage把单独的3D的分辨率这个会导致upscalepass消耗

Blueprint Cluster

开启后,蓝图资源释放问题,大量的蓝图资源和贴图资源,无法被释放

Mesh LOD

低端设备上只加载特定的LOD, r.FreeSkeletalMeshBuffers释放cpu侧的SkeletalMesh的顶点相关数据

    游戏引擎中之所以要做内存管理,一个是加快内存分配速度,另一个就是处理内存泄漏问题。     1.先简单说处理内存泄漏这个问题,一般的引擎在debug 模式下 都有一个记录内存分配的结构体,每分配一段内存就记录这段内存的信息,包括大小,分配时间,是否是数组,前后越界的标记等等吧,其实这些都不是那么重要,因为你只知道这些,一旦泄漏出现,你虽然知道泄漏,但无法定位。相反如果你知道堆栈的调用信息,就能准确定位。我以前的实现,在debug下,只记录当前调用new的时候的行号和文件,也就是内部的__FILE__ __LINE___.。我看了同事那个能记录堆栈调用过程,简直觉得很牛逼(其实不是调用堆栈,只是打印出调用过程,继续往下看你就知道了),以前自己也想过,但不知道怎么去实现。如果U3里面也加入这个功能,那就更牛叉了。思想很简单,就是核心东西在一个函数,这个是系统函数,提供当前这行指令所在的地址,它会打印出来这行指令的文件名和行号。先详细说下数据在内存的分配     最早的计算机数据段和代码段区分的很严格,现在似乎没有这么严格了!对于全局变量和静态变量它的分配完全在数据段分配,知道运行结束才会收回内存!而对于自动变量(包括函数参数和函数中定义的变量)则在堆栈中分配!一般的分配情形是这样的:从栈下到栈顶依次是函数参数,函数结束后下一条指令的地址,寄存器保护,然后是函数中定义的变量!
### Unreal Engine 内存优化插件 在开发基于 Unreal Engine 的项目时,内存管理是一个至关重要的方面。虽然官方文档并未直接提及特定的插件名称来解决内存优化问题,但可以通过一些社区推荐的方法以及内置功能实现这一目标。 #### 社区内存优化工具 某些开发者创建了专门用于分析和减少游戏运行期间内存占用的第三方插件。这些插件通常通过提供详细的内存报告、资源泄漏检测等功能帮助开发者定位性能瓶颈。例如,“Memory Profiler Plugin” 是一款流行的开源插件,它能够实时监控应用程序中的内存分配情况并生成可视化图表[^4]。 #### 官方支持的功能改进 除了依赖外部解决方案外,Epic Games 不断增强引擎本身以更好地处理复杂的场景数据结构。最近版本中引入了一种新型高级中间表示法,旨在统一网格物体资产的表现形式从而简化工作流程并开启更多创新特性的大门[^2]。这种变化间接促进了整体效率提升因为减少了转换过程中不必要的计算开销。 另外,在材质编辑器里新增加了一个选项允许艺术家们定义自定义UV通道数量而不受传统限制约束;这不仅提高了灵活性而且有助于降低纹理映射过程中的冗余存储需求。 #### 脚本级优化建议 对于脚本编写者来说,合理利用蓝图系统内的新特性同样能带来显著效果。比如现在可以在本地C++类属性上应用“Bitmask”元标签或者借助图形化界面勾选对应字段来自动生成符合逻辑运算规则的数据类型实例对象[^3]。配合专用构建块如 “Make Bitmask” 和布尔反向操作符一起使用,则可进一步精简条件判断语句长度进而节约CPU周期消耗量。 ```cpp // Example C++ snippet demonstrating bitmask usage within native code. UENUM(BlueprintType, Meta=(Bitflags)) enum class EMyFlags : uint8 { None = 0, FlagA = 1 << 0, FlagB = 1 << 1, All = FlagA | FlagB }; ``` 上述代码片段展示了如何声明一个带有位标志特性的枚举类型以便于后续快速组合多种状态值而无需额外空间成本。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值