CPU优化之加载模块
前段时间打算总结加载模块,UWA的几篇博客讲的比较针对和详细。
博文地址:
性能优化,永无止境—CPU篇
Unity加载模块深度解析(纹理篇)
Unity加载模块深度解析(网格篇)
Unity加载模块深度解析(Shader篇)
具体的内容有兴趣的可以查看下原博文,还有这篇博客总结很好每天一点UWA:加载模块深度解析 :
资源加载
加载模块中最为耗时的性能开销可以归结为以下几类:资源加载、资源卸载、Object的实例化和代码的序列化等。
场景卸载
对于Unity引擎而言,场景卸载一般是由引擎自动完成的,即当我们调用类似Application.LoadLevel的API时,引擎即会开始对上一场景进行处理,其性能开销主要被以下几个部分占据:
- Destroy
引擎在切换场景时会收集未标识成“DontDestoryOnLoad”的GameObject及其Component,然后进行Destroy。同时,代码中的OnDestory被触发执行,这里的性能开销主要取决于OnDestroy回调函数中的代码逻辑。 - Resources.UnloadUnusedAssets
一般情况下,场景切换过程中,该API会被调用两次,一次为引擎在切换场景时自动调用,另一次则为用户手动调用(一般出现在场景加载后,用户调用它来确保上一场景的资源被卸载干净)。在我们测评过的大量项目中,该API的CPU开销主要集中在500ms~3000ms之间。其耗时开销主要取决于场景中Asset和Object的数量,数量越多,则耗时越慢。
场景加载
- 资源加载
资源加载几乎占据了整个加载过程的90%时间以上,其加载效率主要取决于资源的加载方式(Resource.Load或AssetBundle加载)、加载量(纹理、网格、材质等资源数据的大小)和资源格式(纹理格式、音频格式等)等等。
Loading.UpdatePreloading:这一项仅在调用类似LoadLevel(Async)的接口处出现,主要负责卸载当前场景的资源,并且加载下一场景中的相关资源和序列化信息等。下一场景中,自身所拥有的GameObject和资源越多,其加载开销越大。
Loading.ReadObject:这一项记录的则是资源加载时的真正资源读取性能开销,基本上引擎的主流资源(纹理资源、网格资源、动画片段等等)读取均是通过该项来进行体现。可以说,这一项很大程度上决定了项目场景的切换效率。 - Instantiate实例化
在场景加载过程中,往往伴随着大量的Instantiate实例化操作,比如UI界面实例化、角色/怪物实例化、场景建筑实例化等等。在Instantiate实例化时,引擎底层会查看其相关的资源是否已经被加载,如果没有,则会先加载其相关资源,再进行实例化,这其实是大家遇到的大多数“Instantiate耗时问题”的根本原因,这也是为什么我们在之前的AssetBundle文章中所提倡的资源依赖关系打包并进行预加载,从而来缓解Instantiate实例化时的压力(关于AssetBundle资源的加载,则是另一个很大的Story了,我们会在以后的AssetBundle加载技术专题中进行详细的讲解)。
Ps:Instantiate实例化的性能开销还体现在脚本代码的序列化上,如果脚本中需要序列化的信息很多,则Instantiate实例化时的时间亦会很长。最直接的例子就是NGUI,其代码中存在很多SerializedField标识,从而在实例化时带来了较多的代码序列化开销。
纹理篇
- 同一的格式下(ETC1(Android)和PVRTC(iOS)、且关闭Mipmap功能),图片分辨率越大加载越慢,但是在Android上中(红米NOTE2)高(S6)端机在加载效率上差不多。
- 同一分辨率(1024x1024)上不同格式(对于Android平台,使用ETC1、ETC2、RGBA16和RGBA32四种格式,对于iOS平台,使用PVRTC 4BPP、RGBA16和RGBA32三种格式)
内容差异:
1. 三组纹理的内存占用分别为1MB、1MB、4MB 和 8MB(Android平台)
2. 1MB、4MB 和 8MB(iOS平台))。
加载效率上,ETC1、ETC2、RGBA16基本一致;PVRTC 4BPP、RGBA16基本一致。 - 开启Mipmap功能会导致资源加载更为耗时,且设备性能越差,其加载效率影响越大。打破了Android上同一分辨率下ETC1、ETC2、RGBA16基本一致的情况,需要慎用。
- PS:压缩后的文件大小,和解压缩的效率与纹理的内容相关,这是因为图片的压缩本质是用矩阵对图片数据进行变换,最终得到一个较小的数据。解压缩也是一样,数据的内容影响了反向计算的效率。
UWA给出的建议
- 严格控制RGBA32和ARGB32纹理的使用,在保证视觉效果的前提下,尽可能采用“够用就好”的原则,降低纹理资源的分辨率,以及使用硬件支持的纹理格式。
- 在硬件格式(ETC、PVRTC)无法满足视觉效果时,RGBA16格式是一种较为理想的折中选择,既可以增加视觉效果,又可以保持较低的加载耗时。
- 严格检查纹理资源的Mipmap功能,特别注意UI纹理的Mipmap是否开启。在UWA测评过的项目中,有不少项目的UI纹理均开启了Mipmap功能,不仅造成了内存占用上的浪费,同时也增加了不小的加载时间。
- ETC2对于支持OpenGL ES3.0的Android移动设备来说,是一个很好的处理半透明的纹理格式。但是,如果你的游戏需要在大量OpenGL ES2.0的设备上进行运行,那么我们不建议使用ETC2格式纹理。因为不仅会造成大量的内存占用(ETC2转成RGBA32),同时也增加一定的加载时间。下图为测试2中所用的测试纹理在三星S3和S4设备上加载性能表现。可以看出,在OpenGL ES2.0设备上,ETC2格式纹理的加载要明显高于ETC1格式,且略高于RGBA16格式纹理。因此,建议研发团队在项目中谨慎使用ETC2格式纹理。
网格篇
总结一下:网格的加载效率的影响因素在于网格面数、顶点信息(tangent、color)数和read/write是否开启。论很简单,面数多、信息多、开启会造成内存的变大和加载的变慢。(这个在前面CPU篇渲染模块有详细介绍)
建议
- 在保证视觉效果的前提下,尽可能采用“够用就好”的原则,即降低网格资源的顶点数量和面片数量;
- 研发团队对于顶点属性的使用需谨慎处理。通过以上分析可以看出,顶点属性越多,则内存占用越高,加载时间越长;
- 如果在项目运行过程中对网格资源数据不进行读写操作(比如Morphing动画等),那么建议将Read/Write功能关闭,既可以提升加载效率,又可以大幅度降低内存占用。
Shader篇
- Shader资源的效率加载瓶颈并不在其自身大小的加载上,而是在Shader内容的解析上。
- 总结一下:加载耗时Mobile-Bumped Diffuse > Mobile-Diffuse > Mobile-VertexLit > Mobile-Particles Additive依次减少,对于的normal版本也是一样。
影响shader解析的因素
- 一般情况下,Shader加载的CPU耗时与其Keyword数量有关,Keyword数量越多,则加载开销也越大。(具体数据看原文)
- Shader的Keyword数量是会随着场景设置的不同而变化的。在Unity 5.x中,Unity默认会根据场景设置、Shader Pass等来调整Shader的Keyword,比如如果存在Lightmap的使用,则会默认将对应的Keyword打开,而对于没有使用Fog的项目,则会直接将相关Keyword关闭。
如何提高解析速度:就是减少keyword数量。
- 可以使用skip variant来减少
- 其次去掉Fallback选项,因为文中认为目前不支持Mobile/Diffuse和Mobile/Bumped Diffuse的设备已经相当少,没有必要做这层保险了。
避免重复解析
- 因为一个游戏内用到的shader的数量不会太多,如果shader打包到场景或者模型的AssetBundle中,那么在资源加载卸载的过程中会出现同一个shader多次加载解析的问题。因此建议shader单独打包。
- 注意:对于Unity4.x版本,Shader的AssetBundle加载后只需LoadAll即可完成所有Shader的加载和解析,但对于Unity5.x版本,除执行LoadAllAssets操作外,还需要进行Shader.WarmupAllShaders操作,因为在Unity5.x版本中,Shader的解析和CreateGPUProgram操作是分离的。
- 对于Unity 5.x版本且正在使用Resources.Load来加载资源的研发团队,可以尝试使用ShaderVariantCollection来对Shader进行Preload,同样也可以达到避免相同Shader重复加载的效果。
- 注意:对于Unity5.x以上版本,最好通过AssetBundle来加载和解析Shader,
。
总结
综上所述,shader的处理就是减少keyword、对于简单的渲染去掉Fallback、尽量对shader进行单独打包。