整理自2019.12.15 Unity UUG北京的演讲 讲师:高川(Unity大中华区企业技术支持经理)
什么是内存
物理内存
1.CPU访问内存是一个慢速过程
因此会使用cache来加速访问,大家可以留意CPU的板载示意图,占据最大面积的是Cache模块,如下图
CPU如果在Cache中没有找到数据,称为一次Cache Missing,如果内存数据 指令是不连续的,会导致大量的Cache Missing
2.Unity的ECS和DOTS的目的之一就是提高内存的连续性,减少Cache Missing
3.台式设备与移动设备的内存架构差异很大
4.目前很多厂商提出手游端游化,但是需要注意的是,在技术上是不可能的(注:应该可以理解为使用的优化策略不同)
5.移动设备没有独立显卡以及显存,有的时候,你的内存使用并不大,但还是会内存溢出,例如在Android上,可以看一下有没有OpenGL的out of memory的log,这意味这显存使用太大了
6.CPU板上面积更小,缓存级数更少,大小也更小,例如一台台式机,L3的大小为8-16M,而移动的CPU,例如骁龙845,只有2M,Cache Missing的概率更大虚拟内存
1.内存交换
移动设备不支持内存交换(注:就是把部分物理内存中的数据保存在硬盘上),谷歌的解释是移动设备和台式设备的IO速度是不一样的,因此移动设备在做内存交换的时候会耗费大量的时间在IO上,并且移动设备的存储介质的可擦写次数和台式设备差距也很大
2.iOS可以进行内存压缩
把不活跃的内存压缩起来,放在特定的内存空间中,以节约物理内存,所以在iOS上可以看到有的时候实际使用的内存比物理内存大很多内存寻址范围
1.32位CPU和64位CPU
严格来说,它们的寻址范围是无法确定谁高谁低的,CPU的位数指的是运算位数,不是MCU位数,只是目前大部分是对应的,即32位CPU对应32位的寻址范围,64位CPU对应64位的寻址范围
Android的内存管理
Android的内存大,但是管理不太好
内存的基本单位 Page
1.默认4K一个page(与linux相同),但并不意味是4K对齐的
2.回收和分配以page为单位
3.分为用户态和内核态,常见的一个问题是,用户态中的一个指针越界到内核态,该程序就会挂掉内存管理工具-low memory killer(AKA lmk)
一些现象都和它有关,例如闪退 服务消失 手机重启等
Android内存分为9层,如下图
当内存不足时,killer会从下往上杀应用,当杀到前台(Foreground)时,你的应用就会闪退,如果继续使用内存,会一致杀到System层,这时设备会重启内存指标 *SS
1.Unique Set Size (USS)
当前应用分配的所有内存,这项指标是完全由开发者控制的内存指标
2.Proportional Set Size (PSS)
除了应用分配的内存,即USS外,还会加上应用调用的公共库或公共服务的公摊内存,例如应用本身分配了100M内存,调用了一个Google Service,该Service分配了10M内存,当前调用该Service的应用共10个,即每个应用均摊1M内存,因此PSS就是100+1,即101M,如果此时又有一个应用调用了该Service,均摊大小还会继续减少,值得注意的是,PSS较为准确的反应了应用对系统造成的压力,所以在很多系统中都是用PSS作为指标,如果PSS很大,USS很小,应该检查是不是调用了某个内存占用很大的公共服务
3.Resident Set Size (RSS)
当前应用使用到的所有内存,例如自身分配了100M,调用了Google Service,该Service分配了10M,那么RSS就是100+10,即110M,由于Service可以被很多应用使用,因此该指标的意义不大
Unity内存管理
Unity是C++引擎
主要由三层构成
1.底层Runtime全部有C++构成
2.中层为bonding层(注:不确定是不是这个单词,就是一个粘合层),以前使用Unity自定义的语言,但是因为开发不方便,现在主要使用C#了,作用是将C#和C++连接在一起,大家用到的Unity的API都是在这一层提供的,底层运行的还是C++,这只是一个warpper封装
3.上层就是C#构成的用户代码用户代码在il2cpp模式下会转换为cpp代码
VM依然存在
il2cpp本身也是一个VM,目的是跨平台(注:il2cpp使用到了基于LLVM的技术实现跨平台编译,感兴趣的可以自行百度)
4.Unity内存分配方式
- Native Memory
- Managed Memory
- Editor
Editor和Runtime是完全不同的,不仅是内存大小不同,分配时机,方式都不同,例如一个asset,在Runtime下,不主动load,不会进内存,Editor下,只要打开Unity,就可能会加载进内存,这种策略是为了保证编辑时的流畅,这种策略也导致首次打开Unity项目耗时特别长,会转换资源,再load相关资源,在2019.3中有了新的策略,按需导入和加载
5.Unity内存管理方式
- 引擎管理内存
- 用户管理内存
即profiler中C#分配的托管内存
6.Unity监测不到的内存
- 用户分配的native内存
例如自己写的一个C++插件分配的内存 - lua分配的内存
Unity Native Memory
Unity重载了C++所有内存分配的操作符
- Allocator与memory lable
memory lable是内存操作符需要的一个参数,就是在profiler中的各种lable,作用是将这块内存分配到哪一个类型的Allocator池中,每一个Allocator池单独做自己的跟踪 - NewAsRoot
所有的Allocator的生成都是在执行NewAsRoot的操作符的前提下生成的,NewAsRoot会生成memory island作为一个root,在这个root下面会有很多子内存,例如一个shader,加载一个shader的时候,会生成该shader的root,每个shader会有很多的子数据,例如subshader pass等,会作为该root的子内存,在统计Runtime的内存时,只会统计root - 会及时返还给系统
Native内存最佳实践
Scene
注意scene中GameObject的数量是否过多,数量过多会导致native内存显著增涨,在创建一个GameObject的时候,Unity会在C++中构建一个或者多个的Object来保存相关信息,因此,当发现Native内存过大时,优先检查Scene中的GameObject数量Audio
1.DSP buffer
Unity中对应多档的设置,当需要播放声音时,会向CPU发送对应指令,如果指令发送太频繁,会导致CPU和IO压力 ,因此很多的音频插件,例如Unity中使用的,会使用DSP buffer,当这个buffer被填满之后,再发送指令,所以当这个buffer越大,CPU压力越小,内存占用越多,声音延迟也越大,因为每次都要等这个buffer被填满后才能播放,在一些Android设备上常常出现声音延迟过大,可以优先看看这个选项
2.Force to mono
很多的音频都是双声道的,但是左右声道完全一致,这就会导致内存和包体空间的浪费,在这种情况下,开启这个选项,会强制为单声道,减少内存和包体大小,对音质要求不高的项目可以使用(注:测试发现,只是把音频文件变为单声道,实际播放的时候,例如是双声道播放,还是双声道的,只是播放的是一样的)code size
很多人忽略的问题,代码本身也占内存(也会导致cache missing),其中一个主要的问题是模板泛型的滥用,编译C++时,会把所有的泛型展开为静态类,如果一个类使用了四个泛型,编译出来的cpp文件可能高达25M,这对il2cpp的编译速度造成很大影响,因为一个单一的cpp文件,是无法并行编译的AssetBundle
1.TypeTree
用于不同版本构建的AssetBundle可以在不同版本的Unity上保持兼容,防止序列化出错,如果Build AssetBundle的Unity版本和运行时的版本一致,可以关闭这个功能,关闭之后有三个好处
a.减少内存占用
b.减小包体大小
c.build和运行时会变快,因为当需要序列化有TypeTree的AssetBundle时,会序列化两次,先序列化TypeTree信息,再序列化数据,反序列化也需要两次
2.LZ4&Lzma
LZ4是一种trunk-base的压缩技术,速度几乎是Lzma的10倍,但是压缩的体积会高出30%,trunk-base的压缩方式,在解压时可以减少内存占用,因为不需要解压整个文件,解压每个trunk的时候,可以复用buffer(在中国增强版中会推出一个基于LZ4的AssetBundle加密功能)
3.Size&Count
就是AssetBundle的颗粒度控制,尽量减少AssetBundle的数量,可以减少AssetBundle头文件的内存和包体大小占用,有的资源的头文件甚至比数据还大,官方建议一个AssetBundle的大小在1-2M之间,不过这个建议是考虑网络带宽的影响,实际使用可以根据自身的环境设置Resources文件夹
能不用就不用,在打包的时候,Unity也会为所有的Resources下面的资源构建一个头文件,一棵红黑树(R-B Tree),在游戏启动的时候就会加载进内存,并且不会卸载,因此也会拖慢启动速度,因为红黑树没有加载分析完,是不会进入游戏的,目前这种方式主要用于Debug,甚至一些公司在Debug也不会使用Resources,而使用AssetBundle了Texture
1.upload buffer
和DSP buffer类似,就是填满多少Texture数据时,向GPU push一次
2.r/w
如果没有必要就不要开启,一个Texture正常的加载流程为
加载进内存 -> 解析 -> upload buffer -> 从内存中delete
开启选项后,不会从内存delete,导致内存和显存中都存在一份(注:貌似iOS不会存在两份,而是使用一个虚拟指针,指向同一块数据,具体细节可以查证一下)
3.mip maps
例如UI这些就别开启了,也能减少内存占用Mesh
1.r/w
和Texture r/w类似,能不开就不开
2.compression
需要注意的是,在某些版本中,开了还不如不开,需要自己测试一下Assets
可以看看Unity官方的最佳实践
Unity最佳实践
Unity Managed Memory
VM内存池
1.VM会返还内存给OS吗?
会的,条件是同一个内存block,6次GC都没有访问到时,就会返还,所以概率很小,特别是mono,il2cpp几率还会高一点
2.当VM内存池高于某个阈值时,会根据一些条件,乘出一块内存
3.注意,有时候托管内存已经释放了,实际内存可能还会涨,因为内存碎片化的问题,导致一些内存块无法复用,建议操作内存时,先使用大内存,再使用小内存GC机制考量
1.Throughput(回收能力)
一次回收,能够回收多少的内存
2.Pause times(暂停时长)
回收时,对主线程的影响有多大
3.Fragmentation(碎片化)
回收之后,回收的内存会对整体碎片化贡献多少
4.Mutator overhead(额外消耗)
回收行为本身的消耗
5.Scalability(可扩展性)
能否扩展到多线程
6.Portability(可移植性)
能否在不同平台使用Boehm
Unity当前使用的GC算法
1.Non-generational(不分代式)
分代的特征是指:例如会将大块内存 小内存以及超小内存 长久内存(例如一块长时间未访问的内存会移入长久内存)会放在不同的内存区域管理,Unity未采用的一个考量是,不分代式的速度很快
2.Non-compaction(非压缩式)
压缩是指:当一块内存被回收时,会移动其他内存,使之紧密连接,Unity目前不会压缩,会把它空着,如果下次分配的内存小于空着的内存,就会再次使用
3.为什么选择这种听起来不合理的GC算法呢?
a.历史原因(Unity和Mono的恩怨,导致一直使用老版本的Mono)
b.目前Unity的重点转向了il2cpp,采用了Incremental GC(渐进式GC),解决主线程卡顿问题,原理是分帧进行,将一次卡顿峰值平摊到多帧里面,平摊卡顿时间
c.未来考虑使用SGen算法或升级Boehm?
SGen是一种分代的GC算法,可以减少碎片化,调用执行快,或者考虑升级Boehm算法
d.目前il2cpp上面是Unity自己写的Boehm算法,在策略上会更激进Zombie Memory(僵尸内存)
1.无用内存
代码设计不好,以为可以释放,但是没有释放的内存,所以大家要关注活跃度不高的内存
2.通过代码管理和性能工具来分析(注:后面的Unity广告)
Managed内存最佳实践
- Don’t Null it,but Destroy it
不要置空就完事了,记得显式调用Destroy - Class VS Struct
可以关注Unity的DOTS和ECS - Closures and anonymous methods(闭包和匿名函数)
- Coroutines(协程)
协程可以看作闭包和匿名函数的特例,在il2cpp中,每一个闭包和匿名函数,都会new一个对象出来,只是无法访问,里面的数据,即使是你认为用完就丢的局部变量,在你用完了之后,也不会立即释放,而是等到这个对象释放才释放,有的项目在游戏一开始就开启一个协程,一直到游戏结束,这样使用是错误的,会导致闭包中的数据一直占用内存,正确的做法是用到的时候生成一个协程,不用的时候就扔掉,协程的设计不是当作线程使用的 - configurations(配置表)
如果配置较大,不要一下全部加载进内存,有两个解决方案:
1.通过网络流量获得相关的配置信息
2.按需加载,例如进入一个关卡时,再加载这个关卡的配置 - Singleton(单例)
一定要慎用,在C++的年代,这就是万恶之源,不要什么都往这里面扔,会导致内存无法释放,注意单例的引用关系,当引用关系变得复杂时,很难确定哪些东西没有及时释放
Unity的广告
主要是中国增强版的广告,支持2017.4和2018.4,有几个工具(注:不知道加壳算不算)
- 内存及性能工具-UPR
就是可以在网站上看性能报告的 - 商汤的AR解决方案
- 一个内置的,可以个il2cpp和mono代码加密的工具,支持Android和iOS
- 云构建工具,以及云导入 云编译 云烘焙方案
- crash report,特点是服务器部署在中国,还有就是可以还原Unity的源代码