在开发游戏时,程序性能是需要着重考虑的问题,因为要尽可能覆盖最多的用户群体,就要考虑那些中低端设备的运行效果,兼容非常多配置差异的硬件,在这种情况下,怎么样分析和优化游戏的性能瓶颈是关键。
在运行时把更多的资源加载至内存中,本质上是一种空间换时间的思路。因为频繁从磁盘进行IO是非常耗时的,把资源预先加载到内存就可以实现高速读取,但是内存资源也是有限的,并不能不加限制地使用,尤其是对某些中低端移动设备而言,4G甚至更小的内存的设备目前还具有不少的占有率,所以在内存方面不能浪费,而且过高的内存占用也有可能导致被系统查杀。
内存优化本质上就是在加载效率和内存占用之间寻求一个平衡,怎么样在能满足兼容更多低配设备正常运行不触发OOM的同时尽可能地把可利用的内存使用起来,提高程序的运行效率。
准备写几篇性能优化相关的文章,本篇文章先从UE内存分析入手,介绍常用的内存分析工具和方法,以及对UE项目中能够进行的内存优化手段做一个整理,这部分内容之前以笔记的形式记录在notes/ue中,后续内存相关的内容都会补充到本篇文章。
内存优化其实主要就是从以下四个方面着手:
- 排查内存泄漏
- 裁剪多余模块
- 优化现有模块的内存占用
- 有损优化:砍内容(素材质量等)
三步走:查bug、挤水分、砍需求。
内存分析工具
所以,在进行内存优化之前,首先要能够对UE项目的内存分布有一个大概的了解,可以使用UE提供的内存分析工具以及一些Native平台的分析工具。
内存分析资料:
一些常用的console command:
1 | stat memory #显示引擎中各个子系统的内存占用 |
以及开启LLM,在启动时加上-LLM
参数
1 | -LLM #启用LLM |
然后就可以在运行时使用以下console命令:
1 | stat llm #显示LLM摘要。所有较低级别的引擎统计信息都归入单个引擎统计信息。 |
内存分析还可以使用以下工具:
- memreport
- MemoryProfiler
- Heapprofd(Android)
- Instrument(IOS)
在游戏的console中输入memreport
(-full)会在Saved/Profiling/Memreports
中创建地图目录以及.memreport
文件,可以使用文本编辑器打开,能够看到游戏中各个部分的内存占用情况。
具体的内存分析工具的使用和对UE引擎中内存分配的分析流程有时间再详细补充,关于LLM的内容可以详情参考UE的文档。
内存优化方案
以下列举的优化方式,其实都是可选的,并不是一定要把所有的都做了就最好,因为内存优化要兼顾效率,所以可以根据项目需求在不同的设备上控制要优化的功能,尽可能再保证功能一致地情况下对低端机进行适配。
这里主要列举UE中哪些部分可以被优化以及如果做,具体的优化数据有时间慢慢分析和补充。
关闭不必要的功能支持
根据需求可以裁剪以下的引擎模块支持:
- APEX:如果不使用Nvidia的APEX破碎系统,可以在编译引擎时去掉APEX的支持。可以在BuildSetting或者TargetRules设置
bCompileAPEX=true
。 - Recast(NavMesh):如果客户端在运行时不需要Recast的支持,并且不需要客户端本地进行NavMesh寻路操作,可以运行时裁剪掉NavMesh的支持。可以在BuildSetting或者TargetRules设置
bCompileRecast=true
。 - FreeType:是否需要FreeType字库支持,可以在BuildSetting或者TargetRules设置
bCompileFreeType=true
。 - ICU(unicode/i18n):引擎Core模块中对unicode/i18n的支持,可以在BuildSetting或者TargetRules设置
bCompileICU=true
。 - CompileForSize:UE提供的优化选项,可以控制编译时严格控制大小,但是会牺牲性能。可以在BuildSetting或者TargetRules设置
bCompileForSize=true
。 - CEF3:可选是否支持
Chromium Embedded Framework
,Google的嵌入式浏览器支持。可以在BuildSetting或者TargetRules设置bCompileCEF3=true
。 - bUsesSteam:是否使用Steam,手游可以关闭,在TargetRules中通过
bUsesSteam
控制。 - SpeedTree:如果游戏中不需要使用SpeedTree进行植被建模,可以关闭编译SpeedTree,通过TargetRules中的
bOverrideCompileSpeedTree
控制。 - Audio模块:如果项目使用WWise等作为音频播放接口,如果完全不需要引擎中内置的Audio模块,该部分功能是冗余的,可以裁剪掉。
- 国际化模块:如果游戏的多语言支持不依赖UE的文本采集和翻译功能,可以裁剪掉该模块。
可以减少编译之后静态程序的大小以及减少不必要的执行逻辑。
控制AssetRegistry的序列化
AssetRegistry其实主要是在Editor下用来方便进行资源的查找和过滤操作,它的主要使用者是ContentBrowser,这一点在UE的文档中也有描述:Asset Registry。
对于项目而言在Runtime可能没有需求来使用它,但是在AssetRegistry
模块一启动就会把AssetRegistry.bin
加载到内存中,如果对它没有需求其实这部分内存是浪费的。
好在UE提供了不序列化或者部分序列化AssetRegistry数据的方法,在UAssetRegistryImpl
的构造函数中会调用InitializeSerializationOptionsFromIni
函数来读取DefaultEngine.ini
中的配置,并会构造出一个FAssetRegistrySerializationOptions
结构来存储,它会在后续的Serialize
函数中使用,用来控制把哪部分的数据序列化到AssetRegistry
中。
1 | void UAssetRegistryImpl::InitializeSerializationOptionsFromIni(FAssetRegistrySerializationOptions& Options, const FString& PlatformIniName) const |
这个控制方式可以在打包时控制是否生成AssetRegistry.bin,以及控制在运行时反序列化哪些AssetRegistry的数据(但是不会对DevelopmentAssetRegistry.bin造成影响,可以用它来进行资产审计)。
它的反序列化流程为:
- 检测
bSerializeAssetRegistry
,如果为true
则把AssetRegistry.bin以二进制形式加载到内存中 - 通过
Serialize
函数来把二进制数据反序列化 - 释放加载AssetRegistry.bin所占用的内存
所以,AssetRegistry的内存占用是在序列化之后的数据,而FAssetRegistrySerializationOptions
就是控制把哪些数据序列化的。
1 | /** Load/Save options used to modify how the cache is serialized. These are read out of the AssetRegistry section of Engine.ini and can be changed per platform. */ |
配置的读取在以下代码中:
1 | void UAssetRegistryImpl::InitializeSerializationOptionsFromIni(FAssetRegistrySerializationOptions& Options, const FString& PlatformIniName) const |
在Config/DefaultEngine.ini
中创建AssetRegistry
Section使用上面的名字就可以控制AssetRegistry的序列化,减少打包时的包体大小以及内存占用(AssetRegistry在引擎启动时会加载到内存中)
1 | [AssetRegistry] |
也可以对某个平台来单独指定,只需要修改平台相关的Ini文件:
1 | Config/Windows/WindowsEngine.ini |
只加载所使用质量级别的Shader
默认情况下,引擎会把所有质量级别的Shader加载到内存中,在不需要实施切换画质的情况下,可以不加载未使用的质量级别,降低Shader的内存占用。
在Project Settings
-Engine
-Rendering
-Materials
-Game Discards Unused Material Quality Levels
:
或者在DefaultEngine.ini
中添加以下配置:
1 | [/Script/Engine.RendererSettings] |
When running in game mode, whether to keep shaders for all quality levels in memory or only those needed for the current quality level.
- Unchecked: Keep all quality levels in memory allowing a runtime quality level change.(default)
- Checked: Discard unused quality levels when loading content for the game, saving some memory.
减少Shader变体
可以通过减少母材制的数量以及在Project Settings
-Engine
-Rendering
中开启下列选项:
ShareMaterialShaderCode
在打包时可以在Project Settings
-Packaging
中设置Share Material Shader Code
和Shadred Material Native Libraries
可以减小包体的大小,并且会减少内存占用(增加加载时间)。
1 | /** |
开启了之后打出的包中会生成下列文件:
1 | ShaderArchive-Blank425-PCD3D_SM5.ushaderbytecode |
但是,如果开启之后如果后续的Cook资源Shader发生了变动,而基础包内还是旧的ShaderBytecode信息,会导致材质丢失。
有三个办法:
- 后续的打包时可以把Shaderbytecode文件打包在pak中,挂载时加载;
- Cook热更资源时把Shaderbytecode打包在资源内;
- 创建ShaderPatch,在热更后加载;
热更Shaderbytecode更具体地实践流程可以在我之前对我文章UE4热更新:Create Shader Patch中查看。
关闭UMG的模板化创建
引擎中有缓存蓝图控件加速创建的功能,但是会造成内存的浪费,可以配置关闭:
也可以直接修改引擎中的代码使用类内初始化给予默认值:
1 | UPROPERTY(EditAnywhere, AdvancedDisplay, Category=WidgetBlueprintOptions, AssetRegistrySearchable) |
该变量在以下代码中被检测使用:
1 | bool FWidgetBlueprintCompilerContext::CanAllowTemplate(FCompilerResultsLog& MessageLog, UWidgetBlueprintGeneratedClass* InClass) |
关闭pakcache
引擎中默认启用了PakCache机制,在从Pak中读取文件时,会多读一段内存用作缓存,内存占用还是十分可观的(通过stat memory
查看):
游戏启动时会有PakCache的Log:
1 | [2021.03.23-10.49.21:354][445]LogPakFile: Precache HighWater 16MB |
可以通过以下方式配置关闭:
1 | [ConsoleVariables] |
关闭PakCache会带来频繁IO的问题,但是具体的性能影响细节要等有时间再来分析。
Unload pakentry filenames
从UE4.23开始,引擎中提供了Mount PakFile的内存优化配置:
1 | [Pak] |
在FPakPlatformFile执行Initialize的时候会绑定FCoreDelegates::OnOptimizeMemoryUsageForMountedPaks
,可以调用该Delegate来通知PakPlatformFile来优化已mount的Pak的内存。
1 | void FPakPlatformFile::OptimizeMemoryUsageForMountedPaks() |
- UnloadPakEntryFilenamesIfPossible:允许卸载PakEntry filenames占用的内存
- DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames:卸载PakEntry filename时要保留的目录
- bShrinkPakEntriesMemoryUsage:缩小PakEntry的内存占用
当调用之后,如果开启UnloadPakEntryFilenamesIfPossible
了,会通过计算Pak中文件名列表的Hash来节省内存,但是卸载PakEntry filenames之后无法再使用路径的通配符匹配。
1 | /** Iterator class used to iterate over all files in pak. */ |
压缩Texture
Texture的压缩是有损压缩,能够减小包体大小以及加载到内存中的大小,虽然是有损压缩,但是在移动端质量降低的效果并不明显,可以根据项目情况进行设置。
之前的笔记中,提到过可以在Project Settings
-Cooker
-Texture
-ASTC Compression vs Size
可以设置默认的资源质量和大小的级别:
1 | 0=12x12 |
在Texture的资源编辑中也可以针对某个Texture单独设置:
Lowest->Hightest对应着0-4
的值,使用Default则使用项目设置中的配置。
并且,设置Compression Settings
的类型也会对资源压缩的类型有差别,Default则是项目设置中的参数,如果设置成NormalMap的类型会是ASTC_4x4
的。
UPDATE
使用Github Gist管理的动态更新内容,在国内网络可能会无法查看。