游戏编程模式之数据局部性模式
通过合理组织数据利用CPU的缓存机制来加快内存访问速度。
(摘自《游戏编程模式》)
现代计算机硬件被木桶效应约束着——CPU逐年增长的速度迅猛,而较为遗憾的是RAM访问速度却是逐年迟缓。速度的不匹配使得CPU的能力收到了极大的限制——虽然它处理数据很快,但是获取数据的速度却很慢。这意味着CPU更多的时间是闲置着等待RAM工作的完成。
因此,如何协调CPU和RAM的工作,将是CPU能力最大化的关键。CPU中设计了CPU缓存技术来优化了两者的不平衡问题(并非完全解决)。当CPU从RAM获取数据时,会将一整块连续的内存(通常64到128字节之间去)一次性获取,并存在CPU自带的存储缓存中——CPU从自身硬件获取数据当然比从外部快。当CPU需要的下一个数据恰好已经存在于缓存中(成为CPU缓冲命中),就节省了访问RAM这一步骤。若不存在,则等待RAM工作的完成。
作为游戏程序员,我们无法从硬件下手优化,那么,从上述硬件优化方案中,我们也明白——数据即性能,如果让数据载入内存的结构让CPU缓冲命中次数更多,游戏理应获得更多的性能。
数据局部性模式
使用情境
- 你所要优化的部分性能问题是由于CPU缓存命中低造成的,否则使用此模式无效果。(可以用工具来可视化缓存,例如:Cachegrind)
- C++的抽象化设计和接口设计通常要靠指针来实现,指针意味着跳转取数据,是CPU缓存命中低的最大问题。因此,在这方面需要进行权衡——想要从缓存命中做性能优化,就会牺牲一定的模块化。
示例代码
class Particle
{
public:
void update();
bool isActive()
{
//...
}
}
class ParticleSystem
{
public:
ParticleSystem() : numParticles_(0) {}
void update();
private:
static const int MAX_PARTICLES=100000;
int numParticles_;
Particle particles_[MAX_PARTICLES];
}
void ParticleSystem::update()
{
for(int i=0;i<numParticles_;i++)
if(particles_[i].isActive())
particles_[i].update();
}
上面的粒子系统的update函数中,只有激活的粒子才会调用更新函数update()。然而,这样的遍历同样也变成了跳转操作。如何做对应的优化,提高缓存命中率。给激活粒子和非激活粒子排序或维护两个粒子列表便可实现连续访问。
class Particle
{
public:
void update();
bool isActive()
{
//...
}
}
class ParticleSystem
{
public:
ParticleSystem() : numParticles_(0) {}
void update();
private:
static const int MAX_PARTICLES=100000;
int numParticles_;
int numActiveNum;
Particle sortedParticles_[MAX_PARTICLES]; //已经排好序;前半段是已激活的粒子,后半段是未激活粒子
}
void ParticleSystem::update()
{
assert(numActiveNum<numParticles_);
for(int i=0;i<numParticles_;i++)
sortedParticles_.update();
}
中心思路
- 将指针跳转访问转为数组连续访问
- 如何处理多态
- 避开继承,可以使用类型对象模式
- 为不同的对象类型使用相互独立的数组