目录
一、意图
合理组织数据,充分使用CPU的缓存来加速内存读取。
二、动机
随着CPU速度的递增,我们可以更快的处理数据,但是不能更快的获得数据。
所以一般会从内存获取数据加载到寄存器。
比如:会计需要仓库的文件统计数据。但是仓库有专门的管理员。由于管理员一天只找得到一盒文件。所以不管会计速度多快,一天也就只能统计一盒数据。
优化:如果仓库管理员找到目标数据时,还把周围相关的数据都拿来放在桌子上。正好会计需要的下一盒数据正在里面。那效率就提高了。
这里会计是CPU,会计的桌子就是寄存器,仓库是机器的RAM。仓库管理员就是从主存加载数据到寄存器的总线。
当然,现在就有了优化:CPU缓存技术。芯片内部有一小块存储器。CPU从那里取数据比内存快得多。这个内存使用的是静态RAM,也被称为缓存。(芯片上的被称为L1级缓存)
无论何时芯片需要从RAM获取一字节的数据,它就会自动将一整块内存读入然后放入缓存——通常是64到128字节。这些一次性传输的字节被称为cache line。
如果需要的下一字节数据就在这块上,CPU就可以从缓存中直接读取。成功从缓存中找到数据被称为“缓存命中”。如果不能从中获得而得从主存里取,就是一次缓存不命中。缓存不命中,CPU就会空转。
为了能从cache line读取越多需要的东西,就需要优化组织数据结构,让要处理的数据紧紧相邻。
三、数据局部性
1.简介
现代CPU有缓存来加速内存读取。它可以更快地读取最近访问过的内存的毗邻内存。通过提高内存局部性来提高性能——保证数据以处理顺序排列在连续内存中。
2.何时使用:
第一准则:在遇到性能问题时使用。优化代码不会让你过得更轻松,因为其往往更加复杂且不灵活。
确定性能问题是由缓存不命中引发。
3.注意:
在C++中,使用指针就意味着在内存中跳跃 ,就带来了不可避免的缓存
四、实例代码
1.连续数组
比如需要更新每个敌人的AI,声音,渲染:
这段代码一看没有问题,但是这里每次去的游戏实体组件的指针就是一次缓存不命中。并且每次获取不同游戏实体也是一次缓存不命中。
这里优化这两个不命中:可以将每种组件存入巨大的数组:一个AI数组,一个物理数组,一个渲染数组。这里存的是实体不是指针。
这样优化后,就不是在内存中跳来跳去,而是在数组中做直线遍历。
2.打包数据
假设做一个粒子系统,将所有的粒子放在一个巨大的缓存中。更新方法:
这里检查粒子的标志位,也会将粒子的数据都加载进来,不活跃的粒子越多,就需要跳过的部分越多,就又造成了颠簸缓存了。
优化:提前排序粒子,将活跃粒子放在列表前面。也就不用检车标识位了。
这里不需要每帧都去排序,只需要在激活粒子与反激活粒子是去管理数组就可以了。
3.冷/热分割
有些游戏实体有些状态时每帧都变化的,也有些是整个生命周期只变一次的。这里就可以将数据结构分离成两个部分,一部分保存“热”数据,那些每帧都要调用的数据。另一部分为冷数据,使用次数较少的。
这里冷组件可以在热组件中包含一个指向它的指针。就可以达到不加载但又可以找到的目的。
五、问题
1.怎么处理多态
i.在做内存优化的部分避免使用子类。
ii.为每种类型使用分离数组代替多态
iii.如果不太担心缓存,也可以使用指针的集合
2.游戏实体是如何定义的?
1.如果游戏实体是拥有它组件指针的类
可以将组件实体存储在连续数组中,就优化了组件在内存中的组织。但是很难在内存中移动组件,如果在实体中有指针指向该组件,移动的时候就需要去同步更新指针。
2.如果游戏实体是拥有组件ID的类
使用裸指针的挑战在于在内存中移动它很难。可以使用更直接的方案:使用ID或者索引来查找组件。ID查找可以使用简单的遍历数组,也可以使用哈希表,将ID映射到组件现有位置。
3.如果游戏实体本身是一个ID
如果,所有的状态逻辑都到了各个系统中。游戏实体的存在意义就只是记录哪几个组件构成了这个游戏物体。在组件想交互时,提供别的组件的位置。
这里的问题是查找某一个组件也许会很慢,解决办法是将组件在数组中的索引作为实体的ID。