数据局部性

“通过合理组织数据利用CPU的缓存机制来加快内存访问速度。”

CPU的速度飞速增长,但RAM访问速度却增长迟缓,从1980年,开始芯片速度每年都会起飞,而RAM速度被CPU远远甩在后面。

对刚访问数据的邻近数据进行访问的术语叫做访问局部性(locality of reference)。

当代计算机在其芯片内部的内存十分有限。CPU从芯片中读取数据的速度要快于它从主存中读取数据的速度。芯片内存很小,以便嵌入在芯片上,而且由于它使用了更快的内存类型(静态RAM或称“SRAM”),所以更加昂贵。

当代计算机有多级缓存,也就是你所听到的那些“L1”、“L2”、“L3”等。它们的大小按照其等级递增,但速度却随等级递减。

这一小块内存被称为缓存(特别一提的是,芯片上的那块内存便是L1缓存),任何时候当芯片需要RAM中的数据时,它会自动将一整块连续的内存(通常在64到128字节之间)取出来并置入缓存中。

假如你需要的下一个数据恰巧在这个块中,那么CPU直接从缓存中读取数据,要比命中RAM快多了。成功地在缓存中找到数据被称为一次命中。假如它没有找到数据而需要访问主存,则称之为未命中

连续的数组

class GameEntity
{
public:
 GameEntity(AIComponent* ai,
       PhysicsComponent* physics,
       RenderComponent* render)
 : ai_(ai), physics_(physics), render_(render)
 {}

 AIComponent* ai() { return ai_; }
 PhysicsComponent* physics() { return physics_; }
 RenderComponent* render() { return render_; }

private:
 AIComponent* ai_;
 PhysicsComponent* physics_;
 RenderComponent* render_;
};

class AIComponent
{
public:
 void update() }
 {
 // Work with and modify state... 

private:
 // Goals, mood, etc. ...


};

class PhysicsComponent
{
public:
 void update() }
 {
 // Work with and modify state... 

private:
 // Rigid body, velocity, mass, etc. ...


};

class RenderComponent
{
public:
 void render()
 {
  // Work with and modify state... 
 }

private:
 // Mesh, textures, shaders, etc. ...
};

许多游戏实体将这样进行实现:

while (!gameOver)
{
 for (int i = 0; i < numEntities; i++)
 {
  entities[i]->ai()->update();
 }

 for (int i = 0; i < numEntities; i++)
 {
  entities[i]->physics()->update();
 }

 for (int i = 0; i < numEntities; i++)
 {
  entities[i]->render()->render();
 }

 // Other game loop machinery for timing...

}

这样的内存布局就不太好,可以优化

AIComponent* aiComponents =
  new AIComponent[MAX_ENTITIES];
PhysicsComponent* physicsComponents =
  new PhysicsComponent[MAX_ENTITIES];
RenderComponent* renderComponents =
  new RenderComponent[MAX_ENTITIES];
while (!gameOver)
{
 // Process AI.
 for (int i = 0; i < numEntities; i++)
 {
  aiComponents[i].update();
 }

 // Update physics.
 for (int i = 0; i < numEntities; i++)
 {
  physicsComponents[i].update();
 }

 // Draw to screen.
 for (int i = 0; i < numEntities; i++)
 {
  renderComponents[i].render();
 }

 // Other game loop machinery for timing...

}

包装数据

假设在做一个粒子系统,将所有粒子置入一个大的连续数组中

class Particle
{
public:
 void update() { /* Gravity, etc. ... */ }
 // Position, velocity, etc. ...
};

class ParticleSystem
{
public:
 ParticleSystem()
 : numParticles_(0)
 {}

 void update();
private:
 static const int MAX_PARTICLES = 100000;

 int numParticles_;
 Particle particles_[MAX_PARTICLES];
};

同时例子系统的一个简单的更新方法

for (int i = 0; i < numParticles_; i++)
{
 if (particles_[i].isActive())
 {
  particles_[i].update();
 }
}

更好的办法是可以时刻跟踪被激活粒子的数目

for (int i = 0; i < numActive_; i++)
{
 particles[i].update();
}

快速的更改数组数据

激活目标粒子,与第一个未激活的进行数组元素交换

void ParticleSystem::activateParticle(int index)
{
 // Shouldn't already be active!

 assert(index >= numActive_);

 // Swap it with the first inactive particle right

 // after the active ones.

 Particle temp = particles_[numActive_];
 particles_[numActive_] = particles_[index];
 particles_[index] = temp;

 numActive_++;
}

反激活粒子只需以相反的方式处理

void ParticleSystem::deactivateParticle(int index)
{
 // Shouldn't already be inactive!

 assert(index < numActive_);
 numActive_--;

 // Swap it with the last active particle right

 // before the inactive ones.

 Particle temp = particles_[numActive_];
 particles_[numActive_] = particles_[index];
 particles_[index] = temp;
}

这样比起,操作大量数组内存,进行平移要好的不止一点半点。

冷热分解

内存是连续的,将经常修改的字段放在连续的位置,将不经常修改的放在连续的一起。

class AIComponent
{
public:
  // Methods...

private:
 Animation* animation_;
 double energy_;
 Vector goalPos_;

 LootDrop* loot_;
};

class LootDrop
{
 friend class AIComponent;
 LootType drop_;
 int minDrops_;
 int maxDrops_;
 double chanceOfDrop_;
};