def yasuharu519(self):

日々の妄想

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

Cocos2d-x の v3.4 からカスタムメモリアロケータの仕組みが追加されています。 Cocos2d-xの勉強会に参加した時にConsole機能について知ったのですが、 そのConsoleの中でメモリアロケータの情報を取得することができるようになっており、 今回試してみたくなってためしてみたところです。

tl;td

Cocos2d-x のカスタムメモリアロケーション機能をonにする方法について

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

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

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

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

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

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

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

参考

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

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

v3.4のリリースノートにカスタムアロケータの使い方について書かれているのですが、わかりづらい部分もあったので、順を追って説明します。

使い方

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

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

#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_ALLOCATOR1に設定すると、こちらも設定されます。これを設定することで、Consoleからメモリ状態などを確認することができるようになります。
  • CC_ENABLE_ALLOCATOR_GLOBAL_NEW_DELETE
    • これを1に設定すると、newdelelteCC_ALLOCATOR_GLOBAL_NEW_DELETEで設定したもので置き換えられます。
  • CC_ALLOCATOR_GLOBAL
    • グローバルアロケータの設定。デフォルトではcocos2d::allocator::AllocatorStrategyDefaultが使用される設定となっています。
  • CC_ALLOCATOR_GLOBAL_NEW_DELETE
  • newdelete関数を置き換える際の置き換え方の設定。デフォルトではcocos2d::allocator::AllocatorStrategyGlobalSmallBlockが使用されるようになっています。

Allocator

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

とりあえず試してみる

簡単に動作を試すため、

# 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.cppapplicationDidFinishLaunching関数の中で、

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

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

$ telnet localhost 1234

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

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

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

> allocator

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

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.cppHelloWorldScene.hを以下のように変更しています。

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

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

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

  • static変数の定義

を加えています。

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

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の値を見て、適切な値を設定することにより、無駄なメモリ領域の確保をなくしたり、逆に確保するブロックが足りなくなるといったこともなくなるかとおもわれます。

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