5. 内存优化
内存优化目的是加快IO,防止卡主线程,防止频繁操作(创建/删除)内存,避免内存碎片化和占用过高。
5.1 缓存法
与CPU的缓存计算类似,思路是将需要重复创建的对象缓存起来,销毁时将它放入缓存列表,再次创建时优先从缓存列表中读取。实现伪代码:
Array<Object> _objectCache;
Object CreateObject()
{
// 先尝试从缓存中获取对象
if (_objectCache.size() > 0)
{
return _objectCache.pop_back();
}
return new Object();
}
void DestroyObject(Object obj)
{
_objectCache.push_back(obj); // 删除时将其放入缓存列表。
}
缓存法可以降低内存的创建/删除频率,避免碎片化。常用于数量多且创建频繁的物体,如小兵,NPC,血条,特效,道具,各类图标等等。
5.2 内存池
内存池技术是现代主流引擎的标配,目的是避免内存碎片化,加速内存分配和管理。实现思想通常是由引擎预先创建一块较大的内存(也可动态调整),这块内存通过有效的数据结构和算法策略,统一管理小块内存的分配和回收,并为逻辑层提供内存相关的操作接口。内存池实现的方式很多,各有优劣,不一而足。下图是其中的一种实现方式:
分配的内存分为四个部分:第1部分是内存池结构体信息;第2部分是内存映射表;第3部分是内存Chunk缓冲区;第4部分是可分配内存区。更多参看这里。
5.3 资源管理器
资源管理器是将所有需要用到的文件资源统一管理起来,统一创建,加载,释放,回收等,为的是提高复用率,减少资源冗余和内存开销,也是现代引擎必备的一个模块。假如没有资源管理器,势必会造成资源的冗余,同一份资源可能存在很多份内存数据(下图)。
上图所示中,每个模型(Model)引用了一份网格(Mesh)内存数据,3个模型实例就有3份Mesh内存数据,造成Mesh内存资源的冗余。而有了资源管理器的统一管理,所有引用到文件资源的实例都指向了同一份内存数据(下图),避免了内存资源冗余,降低内存和IO消耗。
资源管理器的实现比较简单,主要是运用模板将物体类型抽象出来,然后每个物体类型用一个map<filePath, objectData>的表存储。具体实现这里不累述。
5.4 控制GC
GC是Garbage Collect的简称,意为垃圾回收,是游戏引擎中采用一定策略回收内存池或托管堆里的无用内存和缓存区无用对象的一种技术。GC机制就是防止内存占用过多过久,是一种自动调节内存占用的常用技法。GC的触发一般分为两种:
- 引擎触发。一般是时间间隔到了,或者内存占有量到了某个阈值,引擎便会触发GC。
- 用户调用。通常引擎也提供了API给游戏应用,以便逻辑层可以控制GC的时机。例如Unity的GC.Collect()接口可以触发GC操作。
但是触发GC需要遍历内存池/托管堆/各类缓存表,还可能引发内存碎片整理操作,所以它需要耗费一定的CPU性能,是引起掉帧和卡顿的罪魁祸首之一。那么,我们就需要在逻辑层采用一些方法,避免触发GC,或者减少触发GC的处理时间。常用的方法:
- 避免频繁创建/删除。这个好理解,频繁创建删除对象,会引起很多内存碎片和无用对象,增加触发GC的几率和时间。
- 帧更新内尽量避免临时对象和创建内存。
- for/while等循环内避免避免临时对象和创建内存。
- 尽量避免申请大块内存。申请大块内存会导致内存暴涨,提升GC的几率。
- 避免内存泄漏。这个需要每个技术人员的职业技能和觉悟,也可以通过一些辅助工具检查内存泄漏,详见1.3。
- 主动调用GC。比如在进入战斗前后,切换场景前后,切换主要界面前后调用GC,可以一定程度上减少内存占用,避免掉帧/卡顿。
5.5 逻辑优化
逻辑优化的目标是尽量避免无用的内存操作,防止内存泄漏,尽快释放内存,减少全局变量的使用,关注第三方库的内存消耗。