Cocos2d-x のカスタムアロケータを試してみた

(この記事は [自身のブログ](http://yasuharu519.hatenablog.com/entry/2015/05/05/144559) の記事 [Cocos2d-x のカスタムアロケータを試してみた](http://yasuharu519.hatenablog.com/entry/2015/05/05/144559) からの転載になります。)

## メモリアロケーションとは

(間違っているところもあるかもしれないので、間違いがあればご指摘いただければ幸いです)

プログラムを動かす際には、プログラムやデータの領域をメモリ上に確保することが必要です。
その際、

- スタック領域にメモリ領域を確保(自動メモリアロケーション)
- ヒープ領域にメモリ領域を確保(動的メモリアロケーション)

の大きな二つがあります。
簡単に言うと、一つ目は`new`を使わずに変数領域を確保する方法、二つ目は`new`を使って変数領域を確保する方法です。

スタック上に確保すると、変数の生存期間がそのスタックだけに限られることもあるため、スタックを出ても領域を確保しておくためには、ヒープ領域に確保することが必要です。(つまり`new`を使って確保する必要がある)

動的メモリアロケーションを使用する場合、確保するメモリ領域が連続して空いている必要があり、その空いている領域を探す必要があります。また、異なるサイズの領域の確保/解放を繰り返すと、メモリのフラグメンテーションが発生し、メモリ自体は空いているものの、連続していないため確保できない、といったことが発生することがあります。

そういったこともあり、コンシューマ向けのゲーム開発では、メモリプールなどを使用したメモリアロケータを自前で作成することが一般的なようです。
また、`new`関数をオーバーロードし、メモリ動作を自前のものにするなどもするようです。(こちらはデバッグのため?)

### 参考
- [その18 最低限のカスタムメモリアロケータ](http://marupeke296.com/CPP_No18_allocator.html)
- [動的メモリ確保](http://ja.wikipedia.org/wiki/%E5%8B%95%E7%9A%84%E3%83%A1%E3%83%A2%E3%83%AA%E7%A2%BA%E4%BF%9D#.E9.9D.99.E7.9A.84.E3.83.A1.E3.83.A2.E3.83.AA.E3.82.A2.E3.83.AD.E3.82.B1.E3.83.BC.E3.82.B7.E3.83.A7.E3.83.B3)
- [カスタムメモリマネージャと高速なメモリアロケータについて](http://www.slideshare.net/alwei/ss-11521742)


## Cocos2d-x でのメモリアロケータ

Cocos2d-x でもデフォルトでは、デフォルトの`new`関数や`delete`関数がメモリ管理に使用されていますが、パフォーマンス向上のためのメモリ管理についての議論がフォーラムでなされ、v3.4からカスタムアロケータの機能が追加されています。

- [Work to improve memory allocation and management in Cocos2d-x](http://discuss.cocos2d-x.org/t/work-to-improve-memory-allocation-and-management-in-cocos2d-x/17442/22)

[v3.4のリリースノート](https://github.com/cocos2d/cocos2d-x/pull/9808/files)にカスタムアロケータの使い方について書かれているのですが、わかりづらい部分もあったので、順を追って説明します。

## 使い方
この説明では、cocos2d-x v3.6を使って説明を行います。

カスタムアロケータのための設定として、`ccConfig.h`ファイルに`define`が追加されています。
デフォルトの設定では

```cpp
#ifndef CC_ENABLE_ALLOCATOR
# define CC_ENABLE_ALLOCATOR 0
#endif

#ifndef CC_ENABLE_ALLOCATOR_DIAGNOSTICS
# define CC_ENABLE_ALLOCATOR_DIAGNOSTICS CC_ENABLE_ALLOCATOR
#endif

#ifndef CC_ENABLE_ALLOCATOR_GLOBAL_NEW_DELETE
# define CC_ENABLE_ALLOCATOR_GLOBAL_NEW_DELETE 0
# endif//CC_ENABLE_ALLOCATOR_GLOBAL_NEW_DELETE

#ifndef CC_ALLOCATOR_GLOBAL
# define CC_ALLOCATOR_GLOBAL cocos2d::allocator::AllocatorStrategyDefault
#endif

#ifndef CC_ALLOCATOR_GLOBAL_NEW_DELETE
# define CC_ALLOCATOR_GLOBAL_NEW_DELETE cocos2d::allocator::AllocatorStrategyGlobalSmallBlock
#endif
```

となっているかと思います。

### 追加されたdefine

- __CC_ENABLE_ALLOCATOR__ 
  - これを`1`に設定することで、カスタムアロケータに関する設定がビルドに加わります。
- __CC_ENABLE_ALLOCATOR_DIAGNOSTICS__
  - デフォルトでは、`CC_ENABLE_ALLOCATOR`と同じ値に設定されるようになっているため、`CC_ENABLE_ALLOCATOR`を`1`に設定すると、こちらも設定されます。これを設定することで、`Console`からメモリ状態などを確認することができるようになります。
- __CC_ENABLE_ALLOCATOR_GLOBAL_NEW_DELETE__
  - これを`1`に設定すると、`new`や`delelte`が`CC_ALLOCATOR_GLOBAL_NEW_DELETE`で設定したもので置き換えられます。
- __CC_ALLOCATOR_GLOBAL__
  - グローバルアロケータの設定。デフォルトでは`cocos2d::allocator::AllocatorStrategyDefault`が使用される設定となっています。
 - __CC_ALLOCATOR_GLOBAL_NEW_DELETE__
  - `new`や`delete`関数を置き換える際の置き換え方の設定。デフォルトでは`cocos2d::allocator::AllocatorStrategyGlobalSmallBlock`が使用されるようになっています。

### Allocator

- __Default Allocator__
  - `malloc`と`free`のラッパー。現状メモリのトラッキングなどはできないが今後追加される可能性もある
- __General Allocator__
  - 固定長サイズのアロケータを複数用意し、一番適したサイズのアロケータを使ってメモリ領域を確保する。`cocos2d::allocator::AllocatorStrategyGlobalSmallBlock`というクラスがこれのことで、`4` , `8`, `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048`, `4096`, `8192`バイトの固定長サイズのアロケータが準備され、これ以上のサイズのものはデフォルトのアロケータが使用されるようになっている。
- __Fixed Block Allocator__
  - 固定長サイズのアロケータ。同じサイズのブロックを複数用意し、そのブロックを確保/開放するので、メモリ領域の検索などが必要ないため動作が速い。
- __Pool Allocator__
  - 特定の型のためのアロケータ。

## とりあえず試してみる

簡単に動作を試すため、

```cpp
# define CC_ENABLE_ALLOCATOR 1
# define CC_ENABLE_ALLOCATOR_GLOBAL_NEW_DELETE 1
# define CC_ALLOCATOR_GLOBAL_NEW_DELETE cocos2d::allocator::AllocatorStrategyGlobalSmallBlock
```

に設定します。このように設定することで、カスタムアロケータ機能が追加され、`new`関数を使った場合も、`cocos2d::allocator::AllocatorStrategyGlobalSmallBlock`が使用されるようになります。

Console機能を追加するため、`AppDelegate.cpp`の`applicationDidFinishLaunching`関数の中で、

```cpp
auto director = Director::getInstance();
auto console = director->getConsole();
console->listenOnTCP(1234);
```

のように設定も行います。このように設定することで、`localhost`の`1234`番ポートを開いた状態でアプリが起動します。
今回のテストでは、`cocos new`コマンドでデフォルトのテンプレートから作られるサンプルアプリケーションを使用します。そうしてiOS版のビルドを行った上で起動し、

```shell
$ telnet localhost 1234
```

とターミナルから実行すると、

```shell
$ telnet localhost 1234
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
>
```

このような画面が表示されると思います。そこで、

```shell
> allocator
```

と実行すると、今回の場合では、

```shell
GlobalSmallBlock::8192 initial:100 count:1 highest:3
GlobalSmallBlock::4096 initial:100 count:8 highest:8
GlobalSmallBlock::2048 initial:100 count:13 highest:14
GlobalSmallBlock::1024 initial:100 count:13 highest:13
GlobalSmallBlock::512 initial:100 count:49 highest:51
GlobalSmallBlock::256 initial:100 count:89 highest:97
GlobalSmallBlock::128 initial:100 count:275 highest:279
GlobalSmallBlock::64 initial:100 count:369 highest:373
GlobalSmallBlock::32 initial:100 count:356 highest:367
GlobalSmallBlock::16 initial:100 count:77 highest:82
GlobalSmallBlock::8 initial:100 count:89 highest:90
GlobalSmallBlock::4 initial:100 count:0 highest:0
GlobalSmallBlock::4 allocated:0
GlobalSmallBlock::8 allocated:712
GlobalSmallBlock::16 allocated:1232
GlobalSmallBlock::32 allocated:11392
GlobalSmallBlock::64 allocated:23552
GlobalSmallBlock::128 allocated:35200
GlobalSmallBlock::256 allocated:22784
GlobalSmallBlock::512 allocated:25600
GlobalSmallBlock::1024 allocated:14336
GlobalSmallBlock::2048 allocated:26624
GlobalSmallBlock::4096 allocated:32768
GlobalSmallBlock::8192 allocated:8192
Total:202,392
```

のような結果となりました。`GlobalSmallBlock::xx`の部分がブロックサイズ、`initial`が最初に確保したブロックの数、`count`が現在の確保ブロック数、`highest`が最大のブロック数のようです。

## プールアロケータの使用

リリースノートに、プールアロケータの導入方法も書かれていましたが、その方法ではうまく行かなかったので、自分がやった方法を補足しておきます。今回はサンプルアプリケーションを使用しているので、`HelloWorldScene.cpp`と`HelloWorldScene.h`を以下のように変更しています。

[gistへのリンク](https://gist.github.com/yasuharu519/12f3adda60dded3acddd)

`HelloWorldScene.h`の変更点としては、

- アロケータ関連のヘッダの追加
- プールアロケータのstatic変数の確保
- `CC_USE_ALLOCATOR_POOL`マクロを使って、確保したstatic変数を使用してメモリ確保を行うように
- `create`関数を用意
  - `CREATE_FUNC`マクロでは `new(nothrow)`を使うようになっており、エラーが出ていたので、マクロを使わず自分で定義しています。

`HelloWorldScene.cpp`の変更点としては、

- static変数の定義

を加えています。

このように変更を行うことで、先ほどと同じようにConsoleからメモリアロケーションの情報を見てみると、

```shell
HelloWorldPool initial:100 count:1 highest:1
GlobalSmallBlock::8192 initial:100 count:13 highest:19
GlobalSmallBlock::4096 initial:100 count:36 highest:54
GlobalSmallBlock::2048 initial:100 count:50 highest:232
GlobalSmallBlock::1024 initial:100 count:214 highest:284
GlobalSmallBlock::512 initial:100 count:59 highest:822
GlobalSmallBlock::256 initial:100 count:907 highest:11583
GlobalSmallBlock::128 initial:100 count:479 highest:41864
GlobalSmallBlock::64 initial:100 count:3661 highest:3867
GlobalSmallBlock::32 initial:100 count:269 highest:697
GlobalSmallBlock::16 initial:100 count:214 highest:300
GlobalSmallBlock::8 initial:100 count:40 highest:528
GlobalSmallBlock::4 initial:100 count:0 highest:0
GlobalSmallBlock::4 allocated:0
GlobalSmallBlock::8 allocated:320
GlobalSmallBlock::16 allocated:3424
GlobalSmallBlock::32 allocated:8608
GlobalSmallBlock::64 allocated:234240
GlobalSmallBlock::128 allocated:61312
GlobalSmallBlock::256 allocated:232192
GlobalSmallBlock::512 allocated:30208
GlobalSmallBlock::1024 allocated:220160
GlobalSmallBlock::2048 allocated:102400
GlobalSmallBlock::4096 allocated:147456
GlobalSmallBlock::8192 allocated:106496
Total:1146816
```

のようになり、`HelloWorldPool`が追加されているのがわかります。

## まとめ

Cocos2d-xのカスタムアロケータについて紹介し、Console機能からアロケーション情報について調べる方法について紹介しました。
追加や削除が頻繁に行われるクラスに対してはメモリプールを導入することで動作も早くなり、フラグメンテーションも起きづらくなることが期待されます。
また、メモリプールもデフォルトでは100ブロック確保するようになっていますが、Consoleからhighestの値を見て、適切な値を設定することにより、無駄なメモリ領域の確保をなくしたり、逆に確保するブロックが足りなくなるといったこともなくなるかとおもわれます。

ゲーム全体にすぐ導入はできないかもしれませんが、パフォーマンス・チューニングのために使ってみるのも良いかもしれません。