UE4内存优化
https://zhuanlan.zhihu.com/p/1087487843
移动平台内存参考
分析工具
ADB
adb shell dumpsys meminfo http://com.xxx.xxx 或者pid
衡量指标:PSS/Private Dirty
- PSS (Proportional Set Size) = 私有内存占用 + 共享内存占用 / 共享进程数
- Private Dirty 值是仅分配给您的应用堆的实际 RAM
- Rss Total =私有内存占用 + 共享内存占用
- Native 分配的堆内存占用大小
- Dalvik Heap 虚拟机堆内存占用大小
- Dalvik 虚拟机的 JIT 和 GC 占用的内存
- .so mmap native 机器码所占内存
- .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结尾的文件里
- Obj List 当前游戏中的蓝图类及其创造的对象和对应数量
- RHI resource memory RHI的Texture,Uniform Buffer,Pixel Buffer,Vertex Buffer
- Levels 当前加载的关卡,类似于stat levels指令显示出来的结果
- Listing all textures. 当前游戏中用到的贴图信息,包括贴图的大小,格式等等,这块内存的估算还算比较准,并且用来排查一些不合理的、较大的贴图占用也是十分的方便。
- ParticleSystems 粒子相关
- 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加入如下配置来优化这部分的内存占用。
- UnloadPakEntryFilenamesIfPossible=true会调用UnloadPakEntryFilenames函数将Pak里的用来检索的Index清理掉,里面创建的Filename字符串也会释放掉,转而用FilenameHashs来代替检索,节省大部分FileSystem的内存占用
- ShrinkPakEntriesMemoryUsage开启后,会将Pak里Entries部分的内存进行压缩
- 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的顶点相关数据