Edited at

[C++]ゲームのオブジェクト管理システムが遅い場合

More than 3 years have passed since last update.

この記事の続き

http://qiita.com/mrdagon/items/c49f23b426aa96694aa2


はじめに

 ゲームのオブジェクト管理を同じ型を静的配列に入れて処理をswitch文で分岐する方式で実装してたのから、仮想関数でポリモーフィズムする感じの設計にして配列をstd::vectorにしてstd::shared_ptr使うようにしたら遅くなって処理落ちしだした、どうしよ?みたいな人向けの記事。

 具体的に言うと、下の方が書かれた記事の例で言う1.から3.にしたら遅くなったので、めんどくさいけど4.をしたい人向けです、先に読んでおくとこの記事を読みやすくなると思います。

ゲームのオブジェクト管理システムについて考える

 また今回の内容は同人2Dゲームを作る等のメモリが何に使うのか分からない程余っている状況を想定しています、メモリがカツカツの場合は話が変わってくると思います。

 自分の場合はゲームスピード64倍速のリプレイモードとか数千発の弾幕形成とかするので、ある程度速度を気にする必要があったんですが、普通はそこまで気にしなくても余裕で60fps出せる事はあります。

 ただ『(並列化関係なく)処理が速い=無駄な処理をしていない=CPUの負荷が下がる=電気消費量が減る』のような効果がありそうなので、スマホ向けとかだと処理落ちしてなくても高速化する重要性がワンチャンあるかも?エコだよこれは。

 コンパイラはMSVC2015のプレビュー版だけ試しました。


色々面倒なポインタ関連

 オブジェクトを全部同じ型にすると、配列に確保するのが楽です。


//Enemyはメンバー変数で処理を分岐する設計
//EnemyはEnemyTypeで初期化出来る的なあれ
Enemy enemyS[10000];
int count = 0;

int main()
{
Add( Enemy(EnemyType::スライム) );
Add( Enemy(EnemyType::ゴブリン) );
Add( Enemy(EnemyType::ドラゴン) );
}

void Add(Enemy &追加する敵)
{
for(int a=0;a<10000;++a)
{
if( !enemyS[a].isUse )
{
//開いてる所に代入
enemyS[a] = 追加する敵;
return;
}
}
}

 静的にメモリを確保する事で急に一時停止する事もなく、敵が発生と消滅を繰り返しても遅くなりませんし、メモリリークとかも気にしなくても良いです、何も難しい事は無いですね。

 一方共通の基底クラスを持つようにして、ポリモーフィズムさせる事によって同じコンテナに入れるような処理をすると、メモリの動的確保が必要になって十倍は遅くなる上ややこしいです。


//ゴブリン、スライム、ドラゴンはIEnemyを継承している
//make_shared内でnewしているので遅い
//shared_ptrを使わずに、newする場合は自分でdeleteする必要があるので面倒だし遅い
int main()
{
std::vector<std::shared_ptr<IEnemy>> enemyS;
enemyS.reserve(10000);
enemyS.push_back( std::make_shared<ゴブリン>() );
enemyS.push_back( std::make_shared<スライム>() );
enemyS.push_back( std::make_shared<ドラゴン>() );
}

 shared_ptrを高速化させるのは結構面倒なので、色々代替方法を紹介します。


デバッグビルドって何ですか?(唐突)

 shared_ptrやstd::vectorやらの話をする前にデバッグビルドとリリースビルドの違いはある程度理解しているか確認します、これがすごく大事なんですね。

 大雑把に言うと、デバッグビルドはデバッグのために余計な処理を入れたり最適化も殆どしないでバイナリを出力するビルド方式で、リリースビルドは処理を高速にするためコンパイラが無駄な処理を最適化してバイナリを出力するビルド方式です。

 開発中はデバッグ作業の効率化のためにデバッグビルドを行い、完成したソフトを配布する時はリリースビルドを行うと言うのがよくあるパターンです。

 例えば『STLは高度な最適化がなされるので、配列とstd::arrayの速度差はありません、なのでstd::arrayを使う”べき”である。』みたいなのはデバッグビルドの事を無視しているので、胡散臭いですね。

 最適化されない場合STLは相当遅いです、特にMSVC(Visual Studio C++)においては顕著です。


const int SIZE = 1000;
const int LOOP = 10000;
std::vector<int> arrayA(SIZE);
std::array<int,SIZE> arrayB;
int arrayC[SIZE];

auto start = std::chrono::system_clock::now();

for (int b = 0;b < LOOP; ++b) {
for (int a = 0; a < SIZE; ++a) {
arrayA[a] = a + b;//a+bにして最適化でコードを消さないように
}
}

auto end = std::chrono::system_clock::now();
std::cout << "vector:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl;

start = std::chrono::system_clock::now();

for (int b = 0;b < LOOP; ++b) {
for (int a = 0; a < SIZE; ++a) {
arrayB[a] = a + b;
}
}

end = std::chrono::system_clock::now();
std::cout << "array:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl;

start = std::chrono::system_clock::now();

for (int b = 0;b < LOOP; ++b) {
for (int a = 0; a < SIZE; ++a) {
arrayC[a] = a + b;
}
}

end = std::chrono::system_clock::now();
std::cout << "配列:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl;

 こんな感じのコードをMSVCで検証したところ。

リリースビルドの配列とarrayは同じ速度、vectorはその1.5倍ぐらい時間がかかり、デバッグビルドの配列はその6~7倍時間がかかり、デバッグビルドのarrayはさらにその10倍、デバッグビルドのvectorはさらにその10倍かかると言った具合に顕著な差が出ました。

 MSVCの場合設定でもかなり差があり、『プロパティ』>『C/C++』>『コード生成』>『基本ランタイムチェック』の項目を『両方 (/RTC1、/RTCsu と同等) (/RTC1)』から『初期化されていない変数 (/RTCu)』にして。

#define _ITERATOR_DEBUG_LEVEL (0)

 を定義するとある程度速度差は改善され、配列:array:vectorで26:322:3142あった差が、31:73:227まで縮まりました。

 後者の設定だと遅いのは遅いですがボトルネックにはならない可能性もありますが、前者の設定だとデバッグでネックになる可能性も十分あります。

 この辺りを説明せずに速度の話をすると。

『(MSVCのデバッグビルドで)今回の方法を使ったら100倍遅くなった(デバッグビルドでは)Cスタイルの方が100倍は早い、この記事はデタラメ。』

『(リリースビルドでは最適化されるので)今回の方法は遅くない(リリースビルドでは最適化されるので)内容は大体合ってる。』

 みたいに残念な感じになってしまいます。

 現実問題としてはデバッグの効率まで考えると、あえてテンプレートを使わずマクロを使ったり、std::arrayを使わずに固定配列を使った方が良い事もあるわけですね。

 で以降の内容ですが、速度の解説においてデバッグビルドを無視すると困る場合もあるのですが、デバッグビルドまで考慮してコードを書くのは面倒臭く、デバッグが遅い場合はコードを部分的に最適化するので細かく知る必要もそんなにありません。

 今回は、(恐らく)デバッグとリリースビルドの速度差が最も激しいMSVCで速度差が許容範囲なら他は大丈夫だろうと言った感じで主にMSVCで検証して書きました。比較グラフみたいなのも面倒だから用意してません。コンパイラによっては気にしないで良いことまで解説してるかもしれません、知らんけど。

 速度の計測はわりと大雑把なので、実測は各自でやろうね。

 速度計測の目安としては、ゲームを60fpsにする場合、1フレームは16.6[ms]なので、1[ms]かかる処理はボトルネックになりえるぐらい重いといった感覚でやれば良いんじゃない?


固定長メモリプールで静的アロケーションする

 newが遅いのは実行時にメモリを確保したり、メモリ確保を複数回に分けて行なうのが主な理由です。リリースビルドでもデバッグビルドでも遅いです。

 一括してメモリを確保するか静的にメモリを確保しておけば速くなるので、メモリプールからplacement newする手法が良く取られます。

 最適化されたメモリプールの実装は相当面倒なのでboost::poolとかを使った方が楽だし速いと思います。boost::poolはヘッダーだけでは動かないタイプなので、その辺りは適当に調べて下さい。

 shared_ptrは内部に参照カウント用にnewでメモリを確保するのでメモリプールと組み合わせる方法がややこしく、boost::intrusive_ptrとかと組み合わせる方が楽です、以下の記事のような感じにしましょう。

boostのobject_poolをスマートポインタで利用する

 さらにいじって、Constract関数で生成して、intrusive_ptr関数からDestroyが呼ばれる感じにすれば良さそうです。


template<int T>
class Size
{
char a[T];
};

//サイズ毎に複数メモリプールを用意しておく
boost::pool<> aPool(8,1000000);
boost::pool<> bPool(16,1000000);
boost::pool<> cPool(32,1000000);
//indexでアクセス出来るようにしておいたり
std::array<boost::pool<>*,3> poolS = { &aPool ,&bPool , &cPool };

class IObj
{
public:
int refc = 0;
virtual void Destroy() = 0;
};

class Obj : public IObj
{
public:
static Obj* Constract()
{
//自身のサイズより大きいプールから確保
//constexperに出来るならconstexperにしたい、constだと遅いかも?
//この場合sizeofが33を超えるとOUT
const int n = std::max( (int)std::ceil(std::log2(sizeof(Obj)-3) , 0 );
return new(poolS[n]->malloc()) Obj();
//return new(bPool.malloc()) Obj();//これでも良いよ
}

void Destroy()
{
const int n = std::max( (int)std::ceil(std::log2(sizeof(Obj)-3) , 0 );
this->~Obj();
poolS[n]->free( this );
//bPool.free(this);//これでも良いよ
}
};

void intrusive_ptr_add_ref(IObj *p_obj)
{
p_obj->refc++;
}

void intrusive_ptr_release(IObj *p_obj)
{
p_obj->refc--;

if (p_obj->refc <= 0)
{
p_obj->Destroy();
}
}

int main()
{
auto start = std::chrono::system_clock::now();

std::vector<boost::intrusive_ptr<IObj>> objS;

for (int a = 0; a < 1000000; ++a)
{
objS.emplace_back(Obj::Constract());
objS.pop_back();
}

auto end = std::chrono::system_clock::now();
std::cout << "処理時間:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl;
}

 constexper対応してないと色々困りますね。

 Constractとかintrusive_ptr_~はテンプレート関数で良いと思います。多分これで十分高速だと思います。

 確保済みのメモリ領域が不足した場合、再確保には現在確保済みの領域に比例した時間がかかるため、あらかじめ十分なメモリを確保しておく必要があるのは注意。

 boost::poolの場合、確保も削除も安定っぽいんですが、似たようなboost::object_poolの方は確保順に削除すると遅くなる?std::vector的?な弱点があるっぽいです?

#include <vector>

#include <chrono>
#include <iostream>
#include <boost/pool/pool.hpp>
#include <boost/pool/object_pool.hpp>

int main()
{
static int N = 10000;

std::vector<int*> vec;
boost::object_pool<int> pool(100000);
//boost::pool<> pool(sizeof(int),100000);//コメントアウト逆にしたらpoolで検証可
vec.reserve(100000);//あらかじめメモリを確保

auto start = std::chrono::system_clock::now();

//N個確保
for (int a = 0; a < 100000; ++a)
{
vec.push_back( new(pool.malloc())int());
}

//こう消せば大丈夫っぽい
for (int a = 100000-1; a >= N; --a)
{
pool.free( vec[a] );
}

//こう消すと遅い
for (int a = 0 ; a < N; ++a)
{
pool.free( vec[a] );
}

auto end = std::chrono::system_clock::now();
std::cout << "処理時間:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl;
}

 上のようなコードで検証して、Nを増やすとパフォーマンスが下がるのが確認出来ました。

 そんな感じなのでobject_poolで無くpoolの方を使えば良いと思います。(singleton_poolとかは途中で飽きたので調べてません。)

 コンストラクトする処理時間は同じ型をvectorに突っ込むパターン比べると2倍ぐらい、shared_ptr使うのに比べると、10分の1ぐらいになりました。

 同じ型方式はswitchで何かするのが仮想関数呼び出しより遅いので2倍差は誤差、shared_ptr使うよりかはあきらかに早くなりました。やったぜ。

 リリース/デバッグで逆転するみたいな事も無いので大丈夫だと思います。

メモリ効率は以下のように低下しますが、画像をメモリに確保する場合に必要な量とかに比べると大した事ないので問題無いでしょう。

①boost::poolで事前に確保する領域で余った分

②intrusive_ptrのための変数とか(③があるので関係無い場合もある)

③型より大きい固定長メモリプールから取る差分


動的型っぽいのを使う

 boost::anyとか使えば大体何でも入るよね。

 とはいえあれは内部でnewしているので速くありません、内部で確保してるそんな感じの型を作れば良さそうです。

 今回のような共通の基底クラスを持たせたい場合だと、内部で確保しているパターンで実装するのも簡単ですね。


template< class TBase, int MaxSize >
/*静的にメモリを確保している動的のような型.*/
/*共通基底クラスと最大の型サイズをテンプレートに入れる*/
class Any
{
private:
char buff[MaxSize];

public:
template < typename T >
Any(const T& src)
{
static_assert( sizeof(T) <= MaxSize, "OOKISUGI");
//継承チェック、constexper関連のアレでMSVCだとアレっぽい
//static_assert(std::is_base_of<TSuper, T>(), "KEISYOU SITENAI");
new(buff) T(src);
}

~Any()
{
((TBase*)this)->~TBase();
}

Any& operator = (Any const &src)
{
if (this != &src)
{
//デストラクタ呼ばれない?
for (int a = 0;a < MaxSize;++a)
{
buff[a] = src.buff[a];
}
}
return *this;
}

template < typename T >
Any& operator = (const T& src)
{
static_assert( sizeof(T) <= MaxSize, "OOKISUGI");//ここにもアサート
*this = new(buff) T(src);
return *this;
}

TBase* operator->()
{
return (TBase*)this;
}
}
};

int main()
{
std::vector<Any<IEnemy,16> enemyS;
enemyS.reserve(10000);//事前にメモリを確保
//push_backよりemplace_backの方がいくらか速い
enemyS.emplace_back( ドラゴン() );
enemyS.emplace_back( スライム() );
enemyS.emplace_back( ゴブリン() );

enemyS[0]->Update();//こんな感じで処理を呼び出せる
}

 constexperの実装が遅れてるコンパイラだとstatic_assertが使いにくくなって困りますね。

 実装も簡単でshared_ptrから置き変えるのが楽なのが良いですね。こんなんで動くの?って感じがします(コピーの所)。キャストが遅いかもなので、内部にTBaseのポインタ持たすとかでも。

 MaxSizeより大きい型を入れようとした場合はstatic_assertします、boost::anyに近い実装にして安全性を高めるとか色々改造出来ると思います。

    template< class TBase , int MaxSize >

class Any2
{
private:
char buff[MaxSize];

class IHolder
{
public:
virtual ~IHolder() {}
virtual IHolder * clone(char* buff) = 0;
virtual TBase* Get(void) = 0;
};

IHolder* content;

template < typename T >
class Holder : public IHolder
{
public:

explicit Holder(T const & src) :
held(src)
{}

explicit Holder(T const && src) :
held(src)
{}

virtual IHolder *clone( char* buff )
{
return new(buff) Holder(held);
}

TBase* Get()
{
return &held;
}

private:
T held;
};

public:
template < typename T >
Any2(T const & src) :
content(new(buff) Holder<T>(src))
{
static_assert(sizeof(Holder<T>) <= MaxSize, "Any2<> MaxSize Over");
}

Any2(Any2 const & other) :
content(other.content ? other.content->clone(buff) : 0)
{ }

~Any2()
{
content->~IHolder();
}

Any2& operator = (Any2 const & src)
{
if (this != &src)
{
content = src.content->clone(buff);
}
return *this;
}

template < typename T >
Any2& operator = ( T const & src)
{
static_assert(sizeof(Holder<T>) <= MaxSize, "Any2<> MaxSize Over");
content = new(buff) Holder<T>(src);
return *this;
}

TBase* operator->()
{
content->Get();
}

};

 多分こんな感じ。

 コンストラクト処理時間は同じ型をvectorに突っ込むスタイルの2倍ぐらいになりました。boost::pool + intrusive_ptrともそんなに違いは無いですね。

 メモリ効率の下がり具合は状況によりまぁす。

①MaxSizeは一番サイズが大きいclassに合わせる必要があるので、それより小さい型は差分が無駄になる

③事前にreserveする分がグローバルなメモリプールと違いコンテナ毎に余る

 クラスの大きさがバラバラだと無駄が大きくなる、MaxSizeが大きいほど速度も落ちる、スーパークラスを持ってない型を入れるとバグる、コピーが深いコピーになる辺りは注意が必要です。

 際立って大きいclassが少しだけある場合は、そういうのだけ動的に確保して突っ込む事も出来ます。


template<class TClass, class TSuper>
class IHolder : public TBase
{
public:
TClass* ptr;

//デストラクタでdelete、コンストラクタでnewするなりなんなり
//TBase* operator ->とかを実装する
}

enemyS.emplace_back( IHolder<めちゃでかいクラス,IEnemy>() );

 こんな感じ?

 多分面倒だと思いますが、char[]を使わず、c++11の新機能unionを使って、ダウンキャスト無しで元のクラスを参照出来る実装方法もあると思います。


その他

 他にはカスタムアロケータを使ってどうのこうのするとか、intrusive_listとか色々方法があるそうです。


ここまでのまとめ

 オブジェクト管理用のコンテナにshared_ptrを使って動的アロケーションするのは速くないので、メモリ効率落ちて良いなら他の方法を使った方が良い事もあるよ。

 intrusive_ptr + メモリプールは導入が若干ややこしいけどintrusive_ptrの効果も得られるしメモリ効率もそれなり調整しやすいよ、Any的なのは導入がヘッダーだけで良いので何も考えず楽に使えるけどメモリ効率が悪くなるパターンもあるしコピーが深いコピーになるので遅めだったりするよ。

 Cっぽい方法からC++っぽい方法に変えた場合、デバッグ時はゴミのような速度になる事があるので、Cで書こうみたいな事もあるよ。


コンテナ選び

 次は配列からvector等に変える事で遅くなる現象について解説します。

 複数の変数を扱うには固定配列を使えば良いですね。

int data_a = 0;//必要な数だけ変数を宣言するのは、大変...

int data_b = 1;
int data_c = 2;
int data[3] = {0,1,2};//4個必要だったり2個で良かったりする場合どうする?

 動的に要素数が変わる場合、配列は使いにくそうです。そういう場合はSTLとか使います。

 STLにはvector、array、listなどなど色々なコンテナがあるし、独自のSTLっぽい実装もあったりします。

 オブジェクト管理用のコンテナに求められる要件っていうとこんな感じですね。

①追加は末尾だけで良い

 途中に挿入したいパターンは少ない、描画順制御が必要ならZバッファとか使うし。

②削除する順番はランダム

 途中の要素を消すのが遅いコンテナは避けたい。

③ランダムアクセスしない。

 list構造でも良い。

④要素数は不定?

 上限を決めたいパターンもあると思う。

⑤高速に動作

 わりと大事、デバッグビルドも一応考慮したい

⑦処理速度は一定にしたい

 平均的には早いけどたまに重くなるとか困る、アクション系のゲームだと特に大事。


コンテナ色々

①std::vector

 事前にreserveすればpush_backにかかる時間を一定に出来るので処理も早い。

 しかしながら途中の要素を普通に削除するのが重いのと、MSVCだとデバッグビルド時は大分遅くなる。

②固定配列

 静的にメモリを確保しているので速いし、デバッグビルドの速度差も小さめ。最適化時もvectorより少し早い。

 追加などの処理でvector::push_backの代わりにデータにフラグを持たせて何かしたりする辺りを自分で実装するのが面倒だしそこで遅くなるので結局速く無い、要素数の上限を決める必要がある。

③std::array

 固定配列と殆ど同じだけど、関数の引数にしたり比較したり代入したりが楽。

 リリースビルドだと同速だが、MSVCのデバッグビルドだと数倍遅いので、便利になっている部分の恩恵が無いなら固定配列のが良かったりする事も。

④std::listやforward_list

 途中要素の削除が早い、forward_listの方がメモリ効率が良い。

 一応、vector等に比べるとメモリ効率は下がる。普通に使うと要素を増やす度にメモリを動的に確保するため遅い、メモリアロケータを使うとか、boost::intrusive_list(あるいはslistがstd::forward_listに相当)を使えば改善出来ると思います。boost::intrusive_listはlistとはデストラクタの呼び出しタイミングが違うので注意、ムーブセマンティクス非対応ぽかったりする。

⑤EASTLのvectorやfixed_vector

 std::vectorに比べると色々例外機構がなかったりする分、MSVCのデバッグビルドではstd::vectorより数倍早い。

 しかしながらC++11でSTLが改善された結果なのか、リリースビルドするとstd::vectorより若干遅かったりする事も、STLと違ってBSDスタイルのライセンス表記が必要になるので注意。

 EASTLでぐぐると色々情報が見つかると思うので、詳しくは自分で調べて下さい。


結局どうするのか?

 なんやかんやで利便性と速度のトレードオフを考えると、std::vectorで良いと思います。削除の件でlist的な何かにした方が良い場合もあります。


vectorが遅い?

 よく分かってない状態でstd::vectorを使って遅くなる原因は大体3つぐらいあります。

1つ目はデバッグビルド、2つ目は事前にreserveしない、3つ目は要素数が多い時の削除です。


リリースビルドは問題ないがデバッグビルドで遅い

 最初に書いたとおり、最適化されないビルドでは極端に遅い場合があります。開発環境の設定をいじると改善されるので、なんとかなる可能性があります。

 EASTL::vectorにするとstd::vectorよりいくらかましになるので、なんとかなる可能性がありますがなんとかならない可能性もあるので、根本的な解決になりません。

 結局vectorとか関係なく、一部の処理がデバッグ時の実行速度を圧迫する場合は、遅い処理の実装だけコード毎に最適化設定するなどして、ボトルネックになっている箇所は最適化させつつ、それ以外の箇所は最適化させずにデバッグ機能を使えるようにしておけば良いと思います、デバッグで遅くなる対策としては面倒ですが一番確実だと思います。


reserveしてない

 vectorは事前に確保したメモリが不足すると、2倍の大きさの連続したメモリを確保して、新しい所に全要素をコピーするので必要な分reserveしとかないと、領域が不足した時に一時的に重い処理が発生する事があります。確保しすぎるとその分メモリが無駄になりますね。


要素数が多い時の削除

 vectorは削除する時後方の要素をコピーして詰める性質があります。

なので要素数Nのvectorの先頭を削除するとN-1回コピーが発生し、要素数によっては激烈に重くなります。vectorを使う場合は削除処理を工夫してごまかす必要がある場合があります。

 基本的には『削除はremove_ifが速いんじゃね?知らんけど。』で良いと思いますが、eraseの場合削除数n☓要素数Nのオーダーが、remove_ifでは要素数Nのオーダーになるだけなので、要素数が多くて削除する数が少ない場合はやっぱり遅い場合があります。

 特にコピーのコストが大きい型をvectorに入れてる場合は注意が必要です。

(eraseやremove_if時に何回コピーされるかは、operator = に何か仕込むと確認出来るゾ)

 vectorから途中の要素を削除する場合、最後尾を消したい要素の場所にコピーしてから消すのが早いです、これならコピーは常に1回ですみます。

 この辺りは要素数が千とか万を超えないと殆ど影響なかったりするので、remove_ifも最後尾コピー+pop_backも殆ど変わらない可能性もあります。

std::vector<int> vec(10000);

//途中の要素を消すと遅い事がある
vec.erase( vec.begin() + 100 );

//最後の要素と交換してから消せば遅くならない
vec[100] = vec.back();//std::swap( vec[100] , vec.back() )だとコピー1回多い
vec.pop_back();

 当然ですが最後尾をコピーして消す場合、コンテナ内での順番が変化するので、要素を添字やポインタで参照してたりする場合は気を付ける必要があります(特にAny的な実装の場合)、2Dゲームでコンテナの先頭から描画みたいな事をしてる場合も描画順が変化して不自然にちらついたりする可能性もあり副作用が多いのであんまりやりたく無いと思います。どうしても削除がネックになる場合はlist的なコンテナを使うと良いと思います。


まとめ

 C++の言語機能を使ってオブジェクト指向的な事をやってもリリースビルドはそれ程遅くなりませんが、デバッグビルドや最適化がまともでないコンパイラのビルドは気を付けないとアホみたいに重くなる事があります、初心者っぽい人がC++遅いって言ってる場合これの可能性があり、最適化されるからうんたらみたいな事は的外れな事が多いです。

 コンテナにshared_ptr入れるアレはそれなりに遅くなる事があります、色々工夫するとメモリ効率は落ちますが速くなる事があります。

 vectorは基本的には速いけど、要素数が増えると削除が遅くなる事もあります。

 今回の内容は要素数が小さい場合とかでは、誤差みたいな高速化にしかならない可能性が高いです。

 なんやかんやで、ややこしいし面倒なのでshared_ptr + make_shared + vector + remove_ifで60fps出せる時はそれで良いと思います。

 お わ り


参考になりそう

ゲーム開発におけるメモリアロケーションの実装について

EASTL から垣間見るゲームソフトウェア開発現場の現状 その 1

C++11で覚醒した共用体の話

Inside Boost.Any

object_poolのdestroy()がなんだか遅い

ゲームプログラミングにおけるC++の都市伝説