《游戏编程模式》笔记——数据局部性
意图
合理组织数据,充分使用CPU的缓存来加速内存读取。
模式
现代的CPU有缓存来加速内存读取。它可以更快的读取最近访问过的内存的毗邻内存。通过提高内存局部性来提高性能——保证数据以处理顺序排列在连续内存上。
何时使用
使用数据局部性的第一准则是在遇到性能问题时使用。
就本模式而言,还要确认你的性能问题确实由缓存不命中引发,如果是其他原因,这个模式帮不上忙。
设计决策
如何处理多态?
不使用多态:
避免子类,至少内存优化的部分避免使用。简洁安全,我们明确的知道处理的是什么类,所有的对象大小相同。更快,动态调用意味着在跳转表中寻找方法,然后跟着指针寻找特定的代码。这个小号会因为不同的硬件区别很大,但动态调用总是会有代价。不灵活,使用动态调用是为了不同对象间展示不同的行为,可以使用虚方法处理。
为每种类型使用分类的数组。对象被紧密排列。静态调度。获得了对象的类型,就不必使用多态,可以使用常规的非虚方法调用。但是需要追踪每个集合,多个类型的时候维护数组会很麻烦。由于我们为每种类型管理分离的集合,我们无法解耦类型集合。
使用指针集合:
使用指针数组指向基类或者接口类型,可以使用多态。
灵活,这样构建集合的代码可以与任何支持接口的类工作,完全开放。
对缓存不友好。指针跳转导致缓存不好有,如果不是性能攸关的,很有可能行得通。
游戏实体是如何定义的?
如果游戏实体是拥有它组件指针的类:
纯OOP的解决方案中,我们拥有游戏对象类,以及指向它拥有的组件的指针,但是不知道组件是如何在内存中组织的。
我们可以将实体存储到连续数组中,游戏实体不在乎组件的位置,我们就可以将组件组织到数组中,优化遍历。
我们拿到一个实体,就可以轻易的获得它的组件,就在一次指针跳转后的位置。
在内存中移动组件很难。组件启用或关闭时,如果想要在数组中移动它们,保证启用的组件位于前列。如果实体中有指针指向组件时直接移动该组件,指针可能会被销毁,得保证同时更新指向组件的指针。
如果游戏实体是拥有组件ID的类:
使用ID或索引来查找组件,需要为每个实体保存独特的ID,遍历数组查找,或者使用哈希表将ID映射到组件现有位置。
但是更复杂,需要实现并排除漏洞,会消耗内存。
需要访问组件“管理器”。基本思路是用抽象ID标识组件,以此来获得对应组件对象的引用。但是需要让ID有办法找到对应的组件。通过裸指针,游戏实体可以直接找到组件,但是需要我们接触游戏实体和组件注册器。
如果游戏实体本身就是一个ID:
实体干的唯一事情就是讲组件连接在一起,定义了一个存在于游戏世界中的实体。
实体很小,想要传递游戏实体的引用时只需要一个简单的值。
实体是空的,必须将所有组件移出,不能再拥有组件独有的状态和行为,这样更加依赖组件模式。
不必管理实体的生命周期,实体是内置值类型,不需要显示分配和释放。实体的所有组件被释放时,对象就隐式死亡了。
查找实体的某一组件可能会慢。为了找某个实体的组件,需要给ID做对象映射,这一过程消耗可能会很大。
也可以使用组件在数组中的索引作为ID,需要我们保持组件数组完全同步,但我们就无法独自排序某个数组。
这一章大部分围绕着组件模式。这种模式的数据结构是为缓存优化的最常见例子。
这一模式几乎完全得益于同类对象的连续存储数组,随着时间推移,我们需要向数组增加或删除对象时,可以使用对象池模式。
游戏引擎Artemis是首个也是最著名的为游戏实体使用简单ID的游戏框架。
参考
《游戏编程模式》