Skip to content

Commit

Permalink
校对
Browse files Browse the repository at this point in the history
  • Loading branch information
tkchu committed Jan 9, 2016
1 parent c33b3f5 commit aa384aa
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 91 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Bob Nystrom同时在Github上无私地提供原本:[munificent/game-programmin
| 重访设计模式 | + | + | + |
| 命令 | + | + | + |
| 享元 | + | + | + |
| 观察者 | + | + |
| 观察者 | + | + | + |
| 原型 | + | + |
| 单例 | + | + |
| 状态 | + | + |
Expand All @@ -44,5 +44,5 @@ Bob Nystrom同时在Github上无私地提供原本:[munificent/game-programmin
| 优化模式 | + | + |
| 数据局部性 | + | + | + |
| 脏标识 | + | + | + |
| 对象池 | + | + |
| 对象池 | + | + | + |
| 空间分区 | + | + |
89 changes: 46 additions & 43 deletions book/object-pool.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

## 意图

*放弃独立的分配和释放对象,从固定的池子中重用对象,以提高性能和内存使用*
*放弃独立地分配和释放对象,从固定的池子中重用对象,以提高性能和内存使用率*

## 动机

我们在处理游戏的视觉效果。
当英雄发射了法术,我们想要在屏幕上爆发闪光。
这需要调用*粒子系统*,由引擎产生的动态闪烁图形,一直显示它们的动画直到不再出现
这需要调用*粒子系统*,由引擎产生的动态闪烁图形,显示动画直到消失

由于一次简单的魔杖挥舞就能产生成百上千的粒子,我们的系统需要能够快速制造他们
由于一次简单的魔杖挥舞就能产生成百上千的粒子,系统需要能够快速制造它们
更重要的是,我们需要保证制造和销毁这些粒子不会造成*内存碎片*

### 碎片的诅咒
Expand All @@ -28,7 +28,7 @@
<aside name="park">

这有点像已经停了很多车的繁忙街道上停车。
如果它们挤在一起,那就有地方,但空闲地带*碎片化*为车之间的空间
如果它们挤在一起,那就有地方,但空闲地带变成了车之间的*碎片*空间

</aside>

Expand All @@ -48,7 +48,7 @@

大多数主机游戏制作者要求游戏通过“浸泡测试”,即让游戏在demo模式运行几天。
如果游戏崩溃了,他们不允许游戏发售。
有时浸泡测试失败是因为很少发生的漏洞,它大多数是因为碎片增长或者内存泄露造成游戏停止
浸泡测试失败很少是因为发生漏洞,大多数是因为碎片增长或者内存泄露造成游戏停止

</aside>

Expand All @@ -58,9 +58,9 @@
简单的方法是最好的——游戏开始时取一大块内存,然后直到游戏结束才去释放它。
但是这对于需要在游戏运行时创建和销毁事物的系统是痛苦的。

一个对象池给了我们两个世界中最好的部分
对象池给了我们两个世界中最好的部分
对于内存管理,我们只需要将一大块内存分出来,然后在游戏运行时不释放。
对于池的用户,我们可以简单的构建析构我们心中内容的对象
对于池的用户,我们可以简单的构建析构我们想要的内容的对象

## 模式

Expand All @@ -71,13 +71,14 @@
当你需要新对象,向池子要一个。
它找到一个可用对象,初始化为“使用中”,然后返回它。
当对象不再被需要,它被设置回“不在使用中”。
这种方式,对象可以轻易的创建和销毁,而不必分配内存或其他资源。
通过这种方式,可以轻易的创建和销毁对象,而不必分配内存或其他资源。

## 何时使用

这个模式在可见事物上,比如游戏实体和视觉效果,广泛使用,但是它也可在不那么视觉化的数据结构上使用,比如正在播放的声音。在以下情况中使用对象池:
这个模式在可见事物上,比如游戏实体和视觉效果,广泛使用,但是它也可在不那么视觉化的数据结构上使用,比如正在播放的声音。
在以下情况中使用对象池:

* 你需要频繁创建和销毁对象
* 需要频繁创建和销毁对象

* 对象大小相仿。

Expand All @@ -87,36 +88,36 @@

## 记住

你通常依赖垃圾回收机制或者`new``delete`来为你处理内存管理
你通常依赖垃圾回收机制或者`new``delete`来处理内存管理
通过使用对象池,你是在说,“我知道如何更好处理这些字节。”
这就意味着处理这个模式局限的责任在你了。

### 池可能在不需要的对象上浪费内存

对象池的大小需要为游戏的需求设置。
当池子太**时,调整是很明显的(没有什么比崩溃更能获得你的注意力)。
当池子太**时,调整是很明显的(没有什么比崩溃更能获得注意力)。
但是小心池子没有太**。更小的池子缓解了可以做其他有趣事情的内存的压力。

### 同时只能激活固定数量的对象

在某种程度上,这是好事。
将内存为不同的对象类型分配分离的池子保证了这点。
举个例子,一连串爆炸不会让粒子系统*消耗*所有可用内存,避免阻碍像创建新的敌人这样的关键事件
举个例子,一连串爆炸不会让粒子系统*消耗*所有可用内存,避免阻碍创建新敌人这样的关键事件

尽管如此,这也意味着试图从池子重用对象可能会失败,因为都在使用中。
这里有几个常用策略来处理:

* *完全阻止这点。*这是通常的“修复”:增长对象池的大小,这样无论用户做什么,它们都不会溢出。对于重要对象,比如敌人或游戏道具,这通常是正确的选择。这也许没有“正确的”方法来处理玩家抵达关底时创建巨大Boss内存不足问题,所以最聪明的办法就是保证这个不发生
* *完全阻止这点。*这是通常的“修复”:增加对象池的大小,这样无论用户做什么,它们都不会溢出。对于重要对象,比如敌人或游戏道具,这通常是正确的选择。这也许没有“正确的”方法来处理玩家抵达关底时创建巨大Boss内存不足问题,所以最聪明的办法就是保证这不发生

这个的反面是强迫你为那些只在一两个危险边缘需要的对象分配过多的内存。由于这个,固定大小的内存池也许不对所有的游戏状态都适用。举个例子,某些关卡也许需要更多的效果而其他的需要声音。在这种情况下,思考为每个场景调整对象池大小
这个的反面是强迫你为那些只在一两个危险边缘需要的对象分配过多的内存。由于这个,固定大小的内存池也许不对所有的游戏状态都适用。举个例子,某些关卡也许需要更多的效果而其他的需要声音。在这种情况下,考虑为每个场景调整对象池大小

* *就不要创建对象了。*这听起来很糟,但是对于像我们粒子系统这样的情况很有道理。如果所有的粒子都在使用,那么屏幕已经充满了闪动的图形。用户不会注意到下个爆炸不像现在运行的这个一样令人印象深刻
* *就不要创建对象了。*这听起来很糟,但是对于像粒子系统这样的情况很有道理。如果所有的粒子都在使用,那么屏幕已经充满了闪动的图形。用户不会注意到下个爆炸不如现在运行的这个引人注目

* *强制干掉一个已有的对象。*思考正在播放声音的内存池,假设你需要播放新声音而池子满了。你*不想*简单的忽视新声音——用户会注意到魔法剑有时会发出戏剧般的声音,有时顽固的一声不吭。更好的解决方法是找到播放中最轻的声音,然后用新的声音替代之。新的声音会覆盖掉前一个声音
* *强制干掉一个已有的对象。*思考正在播放声音的内存池,假设需要播放新声音而池子满了。你*不想*简单地忽视新声音——用户会注意到魔法剑有时会发出戏剧般的声音,有时顽固的一声不吭。更好的解决方法是找到播放中最轻的声音,然后用新声音替代之。新声音会覆盖掉前一个声音

大体上,如果已有对象的*消失*要比新对象的*出现*更不引人察觉,这也许是正确的选择。

* *增加池的大小。*如果你的游戏允许你使用一点内存上的灵活性,我们也许会在运行时增加池子的大小或者创建新的溢出池。如果你用这些方式获取内存,考虑在增加的存量不再需要时,池子是否需要缩回原来的大小。
* *增加池的大小。*如果游戏允许你使用一点内存上的灵活性,我们也许会在运行时增加池子的大小或者创建新的溢出池。如果用这些方式获取内存,考虑在增加的存量不再需要时,池子是否需要缩回原来的大小。

### 每个对象的内存大小是固定的

Expand All @@ -127,10 +128,11 @@

同时,如果你的对象大小是变化的,你在浪费内存。
每个槽都需要能存储最大的对象。
如果对象很少那么大,你每放进去一个小对象就是在浪费内存
这很像是通过机场安检时,使用最大允许尺寸的托盘,里面只放了你的钥匙和钱包
如果对象很少那么大,每放进去一个小对象都是在浪费内存
这很像是通过机场安检时,使用最大允许尺寸的托盘,里面只放了钥匙和钱包

当你发现在用这种方式浪费内存,思考将池为不同大小的对象<span name="pools">分割为<span name="pools">分离</span>的池子——大的托盘给大行李,小的托盘给口袋里的东西。
当你发现在用这种方式浪费内存,思考将池为不同大小的对象<span name="pools">分割为<span name="pools">分离</span>的池子
——大托盘给大行李,小托盘给口袋里东西。

<aside name="pools">

Expand Down Expand Up @@ -165,7 +167,7 @@
但是池仍然是避免构建和析构的有用手段,特别是在有更慢CPU和更简陋垃圾回收系统的移动设备上。

如果你用有垃圾回收的对象池系统,注意潜在的冲突。
由于池不会在对象不再使用的时候真正的析构他们,如果它们仍然保留任何对*其他*对象的引用,也会防止垃圾回收器回收。
由于池不会在对象不再使用的时候真正的析构它们,如果它们仍然保留任何对*其他*对象的引用,也会防止垃圾回收器回收。
为了避免这一点,当池中对象不再使用,清除它对其他对象的所有引用。

## 示例代码
Expand All @@ -188,7 +190,7 @@

^code 2

`create()`函数让其他代码创建新的粒子
`create()`函数让其他代码创建新粒子
游戏每帧调用<span name="update">`animate()`</span>一次,让池中粒子轮流显示动画。

<aside name="update">
Expand All @@ -205,32 +207,33 @@
^code 3

我们遍历池来找到第一个可用粒子。
当我们找到后,我们初始化它然后就完成了
注意在这个实现中,如果这里没有任何可用的粒子,我们就不创建新的
当我们找到后,初始化它然后就完成了
注意在这个实现中,如果这里没有任何可用的粒子,就不创建新的

做一个简单粒子系统的所有东西都在这里了,当然,没有包含渲染粒子。
我们现在可以创建池然后使用它创建粒子。当时间到了,粒子会自动失效。

这足够承载一个游戏,但是敏锐的目光也许会注意到创建新粒子(可能)需要<span name="create">遍历</span>整个集合,直到我们找到一个空闲槽。
如果池子很大很满,这可能很慢。让我们看看我们可以怎样改进这一点。
如果池子很大很满,这可能很慢。
让我们看看可以怎样改进这一点。

<aside name="create">

创建一个粒子需要*O(n)*复杂度,这是给那些忘了我们算法课的人的提示
创建一个粒子需要*O(n)*复杂度,这是给那些忘了算法课的人的提示

</aside>

### 一个空闲列表

如果我们不想浪费时间在*查找*空闲粒子上,明星的答案是不要失去对它们的追踪。
如果不想浪费时间在*查找*空闲粒子上,明星的答案是不要失去对它们的追踪。
我们可以存储分离的指向每个未使用的粒子的指针列表。
然后,当我们需要创建粒子,我们从列表中移除第一个指针,然后重用它指向的粒子。
然后,当需要创建粒子时,我们从列表中移除第一个指针,然后重用它指向的粒子。

不幸的是,这回要我们管理一个和池同样大小的分离数组。
无论如何,在我们创建池时,*所有的*粒子都没有用,所以列表初始会对池中每个对象都有一个指针。

如果能够修复我们的性能问题,*无需*牺牲任何内存就好了。
方便的是,这里已经有我们可以借走的内存了——那些存储未使用粒子本身的内存。
方便的是,这里已经有可以借走的内存了——那些存储未使用粒子本身的内存。

当粒子没有被使用,它大部分的状态都是无关紧要的。
它的位置和速度没有使用。唯一需要的有没有死亡的状态。
Expand All @@ -242,7 +245,7 @@
我们将除`framesLeft_`外的所有成员变量移到<span name="union">union</span>中的`state_`中的`live`结构。
这个结构在动作时保存粒子的状态。
当粒子被重用时,union的其他部分,`next`成员被使用了。
它保持了一个指向这个后面的其他可用粒子的指针
它保留了一个指向这个后面的其他可用粒子的指针

<aside name="union">

Expand All @@ -267,7 +270,7 @@ Unions近些年不那么常见了,所以你可能不熟悉这些语法。

^code 6

现在为了创建新的粒子,我们直接跳到<span name="first">首个</span>可用的:
现在为了创建新粒子,我们直接跳到<span name="first">首个</span>可用的:

<aside name="first">

Expand All @@ -277,11 +280,11 @@ Unions近些年不那么常见了,所以你可能不熟悉这些语法。

^code 7

我们需要知道粒子死亡,这样可将其放回到空闲列表中,所以我们将`animate()`改为在上一个活跃粒子放弃了这帧存在时返回`true`
我们需要知道粒子死亡,这样可将其放回到空闲列表中,所以我们将`animate()`改为在上个活跃粒子放弃存在时返回`true`

^code particle-animate

当那个发生时,我们简单地将其放回列表
当那发生时,简单地将其放回列表

^code 8

Expand All @@ -306,9 +309,9 @@ Unions近些年不那么常见了,所以你可能不熟悉这些语法。

^code 10

在类间保持这种关系来确保你的用户无法创建对象池没有追踪的对象
在类间保持这种关系来确保用户无法创建对象池没有追踪的对象

* *你也许可以避免显式存储“在使用中”标识。*很多对象已经保存了可以告诉外界它有没有在使用的状态。举个例子,一个粒子的位置如果不在屏幕上,也许它就可以被重用。如果对象类知道它会在对象池中,那它可以提供一个`inUse()`来查询那个状态。这省下了对象池存储“在使用中”标识的多余内存。
* *你也许可以避免显式存储“在使用中”标识。*很多对象已经保存了可以告诉外界它有没有在使用的状态。举个例子,一个粒子的位置如果不在屏幕上,也许它就可以被重用。如果对象类知道它在对象池中,那它可以提供一个`inUse()`来查询那个状态。这省下了对象池存储“在使用中”标识的多余内存。

* **如果对象没有和池子耦合:**

Expand All @@ -321,11 +324,11 @@ Unions近些年不那么常见了,所以你可能不熟悉这些语法。
### 谁负责初始化重用对象?

为了重用一个已经存在的对象,它必须用新状态重新初始化。
这里的关键问题是你需要在池类的内部还是外部重新初始化
这里的关键问题是你需要在池子的内部还是外部重新初始化

* **如果池内部重新初始化**
* **如果池在的内部重新初始化**

* *池子可以完全封装它的对象。*取决于你对象需要的其他能力,你可以让它们完全处于池子的内部。这保证了其他代码不会有不小心重用了的对象的引用。
* *池子可以完全封装它的对象。*取决于对象需要的其他能力,你可以让它们完全处于池子的内部。这保证了其他代码不会有不小心重用了的对象的引用。

* *池子绑定到了对象是如何初始化的。*池中对象也许提供不同的函数来初始化。如果池控制了初始化,它的接口需要支持所有的,然后转送给对象。

Expand All @@ -341,14 +344,14 @@ Unions近些年不那么常见了,所以你可能不熟悉这些语法。

^code 14

* *外部代码需要处理无法创建新对象的失败。*前面的例子假设`create()`总能成功的返回一个指向对象的指针。但如果池是满的,它会返回`NULL`。为了安全,你需要在初始化之前检查这一点。
* *外部代码需要处理无法创建新对象的失败。*前面的例子假设`create()`总能成功地返回一个指向对象的指针。但如果池是满的,它会返回`NULL`。为了安全,你需要在初始化之前检查这一点。

^code 15

## 参见

* 这看上去很像是<a class="gof-pattern" href="flyweight.html">享元</a>模式。两者都控制了一系列可重用对象。不同在于“重用”的意思。享元对象通过分享实例间*同时*拥有的相同部分。享元模式在不同上下文使用相同对象避免了*复制*内存使用。
* 这看上去很像是<a class="gof-pattern" href="flyweight.html">享元</a>模式。两者都控制了一系列可重用对象。不同在于“重用”的意思。享元对象分享实例间*同时*拥有的相同部分。享元模式在不同上下文使用相同对象避免了*复制*内存使用。

池中的对象也被重用了,但在不同的时间上。“重用”在对象池中意味着对象在原先的主人用完*之后*分配内存。通过对象池,这里没有期待对象会在它的生命周期中分享什么
池中的对象也被重用了,但在不同的时间上。“重用”在对象池中意味着对象在原先的主人用完*之后*分配内存。对象池没有期待对象会在它的生命周期中分享什么

* 选择内存中同样类型的对象,能帮助你的CPU缓存在遍历这些对象时总是满的。<a class="pattern" href="data-locality.html">数据局部性</a>模式全部关于这一点。
* 选择内存中同样类型的对象,能帮助CPU缓存在遍历对象时总是满的。<a class="pattern" href="data-locality.html">数据局部性</a>模式全部关于这一点。
Loading

0 comments on commit aa384aa

Please sign in to comment.