前言
CPU的速度飛速增長,然而計算機對RAM訪問速度的增長卻很遲緩憔足。CPU速度增長帶來的優(yōu)勢有時并不能為我們的程序帶來同樣的速度提升。
處理器的速度提升,使得我們可以更快的處理數(shù)據(jù)垦搬,但是卻不能讓計算機更快的獲取數(shù)據(jù)。為了讓CPU進行運算艳汽,它需要從主存中取出數(shù)據(jù)猴贰,并放到寄存器中,但是RAM的存取速度遠遠慢于CPU的速度河狐。這就導致CPU可能會花費驚人的時間來等待內存?zhèn)鬏敂?shù)據(jù)米绕。
一個形象的比喻
書中的比喻非常棒瑟捣,想了想,還是大概抄下來把栅干。
設想你是一個小辦公室里的會計迈套。你的工作是采集一盒子的單子并對它們進行一些核查統(tǒng)計或其他的計算。
得益于努力工作碱鳞、出色的才能以及進取心桑李,你可以在(比如說)一分鐘內完成一個盒子里的所有任務。當然劫笙,這里有個小問題芙扎。這些盒子都被分別存放于一棟樓里的某個地方。為了拿到這些盒子填大,你必須詢問倉儲人員來獲取這些盒子戒洼。他去開來叉車并在過道之間尋找直至找到你想要的那個盒子。
他花了一整天的時間來取一個盒子允华。這意味著不論你辦事效率有多高圈浇,你一天只能搞定一個盒子。而剩余的時間靴寂,你就只能坐在辦公椅上思考自己怎么就干了這樣一份傷神的工作磷蜀。
某天,一群工業(yè)設計師出現(xiàn)了百炬。他們的任務是提高工作效率褐隆,比如提高流水線效率之類的工作。在觀察你工作了幾天之后剖踊,他們注意到以下幾點:
- 通常情況下庶弃,在你完成某個盒子里的任務之后,你所需要的下個盒子就放在倉儲間中與這個盒子所在的同個架子上德澈。
- 開著叉車來取個小盒子真是蠢哭了歇攻。
- 其實在你的辦公室角落里有一些空閑的空間。
它們想到了一個聰明的辦法梆造。倉庫管理員為你帶來你要的盒子時缴守,會將與它相鄰的那些盒子也都一起帶來。現(xiàn)在當你需要一個新盒子時镇辉,第一件要做的事情就是查看盒子是否在辦公室的托盤上屡穗。如果在,那就太棒了忽肛!你只需要幾秒的時間把它拿過來然后繼續(xù)你的算數(shù)鸡捐。
CPU的托盤
上面的過程與當今計算機CPU工作原理相似。你相當于CPU麻裁,你的桌面是寄存器箍镜,裝單子的盒子是數(shù)據(jù)源祈,倉庫是機器的RAM,倉庫管理員是讀取數(shù)據(jù)的總線色迂。
CPU內部的內存十分有限香缺,這一小塊內存被成為緩存,這相當于那個裝滿盒子的托盤歇僧。當CPU需要RAM中的數(shù)據(jù)時图张,他它會自動將一整塊的連續(xù)的內存取出來并放到緩存中。
加入你需要使用的下一個數(shù)據(jù)就恰好在緩存中诈悍,那么CPU就能直接從緩存中獲取數(shù)據(jù)祸轮,這是很快的。成功在緩存中找到數(shù)據(jù)被稱為一次命中侥钳,反之則稱為未命中适袜。
當緩存未命中時,CPU就停止運轉舷夺,直到取得數(shù)據(jù)苦酱,我們的任務就是盡量避免這個情況的發(fā)生。
代碼的好壞當然會影響性能给猾,但是疫萤,數(shù)據(jù)的影響同樣非常大,你需要做的就是讓緩存中可用的數(shù)據(jù)盡可能的多敢伸。在緩存中使用的數(shù)據(jù)越多扯饶,程序就跑得越快。
所以優(yōu)化就變成了如何讓要處理的數(shù)據(jù)在內存中兩兩相鄰池颈。如下圖所示尾序,如果你要按順序處理A、B饶辙、C三件事蹲诀,那么斑粱,A弃揽、B、C最好在內存中是這樣布局的:
優(yōu)化
去使用上述優(yōu)化最重要的是要找到需要優(yōu)化的地方则北。并不是所有代碼都需要進行這樣的優(yōu)化矿微,對那些并不會頻繁執(zhí)行的代碼進行優(yōu)化將會浪費你大量的時間,并使你的代碼更加復雜難看尚揣。
當然涌矢,你還要確定程序的性能問題是否是由緩存未命中引起的,如果不是快骗,還是暫時不要在這上面浪費時間了娜庇。
另外塔次,需要注意的是,使用抽象化的結構名秀、使用借口意味著要通過指針或引用來訪問對象励负,這會導致訪問的內存不是連續(xù)的,這就會導致緩存未命中的現(xiàn)象匕得,是否要犧牲一些抽象化的工作來提高效率继榆,需要進行權衡。
示例
在游戲中汁掠,使用組建模式略吨,將不同的模塊拆分:AI、物理考阱、渲染等翠忠。以下是GameEntity類與其他組件類:
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* return render_;
};
class AIComponent {
public:
void update() {
//do something
}
//other members
};
class PhysicsComponent {
public:
void update() {
//do something
}
//other members
};
class RenderComponent {
public:
void update() {
//do something
}
//other members
};
當游戲更新時,我們要分別調用各組建的update函數(shù)進行更新:
while(!gameover){
for (int i = 0;i < num; ++ i) {
entities[i]->ai()->update();
}
for (int i = 0;i < num; ++ i) {
entities[i]->physics()->update();
}
for (int i = 0;i < num; ++ i) {
entities[i]->render()->update();
}
}
上面的代碼不僅引起緩存抖動羔砾,甚至還將它來回攪成一團漿糊:
- 數(shù)組存儲著指向游戲實體的指針负间,因此對于數(shù)組中的每個元素而言,我們需要遍歷這些指針——這就引發(fā)了緩存未命中姜凄。
- 游戲實體又維護著指向組件的指針政溃,再一次緩存未命中。
- 接著我們更新組建态秧。
- 現(xiàn)在我們回到步驟1董虱,對游戲里的每個實體的每個組件都這么干。
上面的代碼不斷做著內存空間一日游的操作申鱼,這可不是一個好的現(xiàn)象愤诱。
為了讓循環(huán)想要訪問的實體都在連續(xù)的內存中,我們可以使用一個保存各類控件的大數(shù)組:
AIComponent* aiComponents = new AIComponent[MAX_ENTITIES];
PhysicsComponent* physicsComponent = new PhysicsComponent[MAX_ENTITIES];
RenderComponent* renderComponent = new RenderComponent[MAX_ENTITIES];
然后我們的遍歷就是這樣的:
while(!gameover) {
for (int i = 0;i < num; ++ i) {
aiComponents.update();
}
for (int i = 0;i < num; ++ i) {
physicsComponent.update();
}
for (int i = 0;i < num; ++ i) {
renderComponent.update();
}
}
沒有了指針來跳躍查找數(shù)據(jù)捐友,而是直接對連續(xù)的數(shù)組進行遍歷淫半,一些小的改變就可以避免內存未命中,使CPU效率得到提升匣砖。