1. サイトトップ
  2. ブログ
  3. C++
  4. 【C++】配置newを使ってみる

【C++】配置newを使ってみる

こんにちは!
制作部プログラマの柄本です。

今回は私にとって初の技術ブログを投稿させていただきます!
今回は「配置new」について概要とメリットを紹介していきます。

配置newとは

皆さんは配置newという機能をご存じでしょうか。私は今回の件で調べるまで知りませんでした。ということでCppReferenceの説明を見てみましょう。

::(オプション) new(placement_params)(オプション) (type)initializer(オプション)(1)
::(オプション) new(placement_params)(オプション) typeinitializer(オプション)(2)
(中略)
placement_params が提供された場合、それらは追加の引数として確保関数に渡されます。 このような確保関数は「配置 new」と呼ばれます。 第2引数を変更せず返すだけの標準の確保関数 void* operator new(std::size_t, void*) に由来しています。 この関数は確保された記憶域内にオブジェクトを構築するために使用できます。

難しい書き方をしているので、以下2点にまとめます。

  • “new” には引数を与えることができる
  • 確保済み領域にオブジェクトを構築するのに使用できる

以下に具体的な例を載せます。

class Hoge {/* ... */};

char* ptr = new char[sizeof(Hoge)];  // メモリの確保
Hoge* pHoge = new(ptr) Hoge;         // 確保されたメモリにHogeオブジェクトを構築(配置)
pHoge->~Hoge();                      // オブジェクトの破棄
delete[] ptr;                        // メモリの解放

こちらが配置newと呼ばれる由来となった、特定のアドレスにオブジェクトを構築(配置)する機能です。配置したオブジェクトと確保したメモリのアドレスが一致します。

この他にも領域が確保できなかった時に例外を投げない代わりに、ヌルポインタを返す new(std::nothrow) T があります。
また、これらの挙動はnew/delete演算子およびnew[]/delete[]演算子をオーバーロードすることで制御できます。

配置newをするメリット

配置newは上記の機能から、メモリの厳密な管理をする場合に大いに役立ちます。最初に大きなメモリプールを確保してその中でメモリを使いまわせば、メモリの使用量の計測・制限を簡単にすることができます。また、用途ごとにプールを分割して使用することなどもできます。
これはメリットとしては大きいものですが、実際に運用しようとするとoperator new/deleteのオーバーロードや確保したメモリの効率的な運用などかなりのコストがかかります。

そこで、他にもメリットはないものかと調べてみたところ配置newは確保済み領域からオブジェクトを構築しているので速度が速いというものがありました。

ということでサンプルプロジェクトを作ってきました!
今回参考にさせていただいたサイトは以下になります。
https://ja.cppreference.com/w/cpp/language/new
https://ja.wikipedia.org/wiki/New%E6%BC%94%E7%AE%97%E5%AD%90
http://d1z.cocolog-nifty.com/blog/2009/09/newplacement-ne.html

以下が今回のソースです。
実行環境はVS2017、Inte(R) Core(TM) i7-9700K CPU @ 3.60GHzです。

// ==============================
// 配置new速度計測テスト
// ==============================
#include <iostream>
#include <chrono>
#include <new>

class Hoge
{
public:
	Hoge() {};
	~Hoge() {};

private:
	char m_pData[64];

};

int main()
{
	std::chrono::system_clock::time_point start, end;	// 処理時間計測用変数
	static const unsigned int MeasureNum = 100;		// 計測回数
	double placementElapsed = 0;
	double usualElapsed = 0;

	static const unsigned int AllocSize = 10 * 1024 * 1024;	// 10MiB、確保するメモリーのサイズ
	std::cout << "メモリープールを確保します。\n";
	void *pPool = std::malloc( AllocSize );		// メモリープールの確保

	static const unsigned int ObjectNum = 50000;	// 構築するオブジェクトの数

													// オブジェクトリスト
	void* ptrList[ObjectNum];
	for( unsigned int i = 0; i < ObjectNum; i++ )
	{
		ptrList[i] = nullptr;
	}

	for( unsigned int num = 0; num < MeasureNum; num++ )
	{
		// 配置newでのオブジェクト構築
		{

			start = std::chrono::system_clock::now();	// 計測開始

			// オブジェクト構築
			std::cout << "オブジェクトを構築します。\n";
			Hoge *ptr = (Hoge*)pPool;
			for( unsigned int i = 0; i < ObjectNum; i++ )
			{
				Hoge *obj = new(ptr) Hoge();			// 確保済みメモリからオブジェクト構築
				ptrList[i] = (void*)obj;
				ptr++;
			}

			// オブジェクト破棄
			std::cout << "構築したオブジェクトを破棄します。\n";
			for( unsigned int i = 0; i < ObjectNum; i++ )
			{
				Hoge *obj = (Hoge*)ptrList[i];
				obj->~Hoge();							// オブジェクトの破棄
				ptrList[i] = nullptr;
			}

			end = std::chrono::system_clock::now();		// 計測終了
			double elapsed = (double)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000.0;
			placementElapsed += elapsed;

			std::cout << "配置new有 : " << elapsed <<"[ms]\n";

		}

		// 普通のnewでのオブジェクト構築
		{
			start = std::chrono::system_clock::now();	// 計測開始


			// オブジェクト構築
			std::cout << "オブジェクトを構築します。\n";
			for( unsigned int i = 0; i < ObjectNum; i++ )
			{
				Hoge *obj = new Hoge();					// メモリを確保し、オブジェクトを構築
				ptrList[i] = (void*)obj;
			}

			// オブジェクト破棄
			std::cout << "構築したオブジェクトを破棄します。\n";
			for( unsigned int i = 0; i < ObjectNum; i++ )
			{
				Hoge *obj = (Hoge*)ptrList[i];
				delete obj;								// オブジェクトを破棄し、メモリの解放
				ptrList[i] = nullptr;
			}

			end = std::chrono::system_clock::now();		// 計測終了
			double elapsed = (double)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000.0;
			usualElapsed += elapsed;

			std::cout << "配置new無 : " << elapsed <<"[ms]\n";
		}
	}

	// 計測結果平均値出力
	std::cout << "配置new有(平均) : " << placementElapsed / (double)MeasureNum <<"[ms]\n";
	std::cout << "配置new無(平均) : " << usualElapsed / (double)MeasureNum <<"[ms]\n";

	std::cout << "メモリープールを解放します。\n";
	std::free( pPool );							// メモリープールの解放

	return 0;
}

そして以下が計測結果になります。

配置new有3.12199[ms]
配置new無15.8423[ms]

うーん…もっと劇的に早くなるかと思っていたのですが、そこまででもない印象を受けます。CPUの性能向上とライブラリの最適化が行われている結果このような結果になったのだと思います。
一応オブジェクトの生成数を変えずにHogeクラスのサイズを大きくしていくと配置new有の方は速度は変わらないですが、配置new無の方は徐々に速度が上がっていきました。しかし、極端に処理速度がかかるということにはなりませんでした。

結果を受けて改めて配置newのメリットについて考えてみると、やはりコストは高いですが厳密にメモリ管理ができるというのが大きいのだと感じます。
速度に関しては大きいデータを大量に扱うときは多少速度の向上が見込めますが、昔ほど劇的に早くなるというものではないという感じです。しかも今回はかなり簡易なサンプルを用いたので、実際に運用する際には速度はそこまで変わらないということもあると思います。

最後に

今回、初めて配置newについて調べたので、ところどころ拙い説明になってしまいました。メモリの管理においてとても有用な機能ではあるのですが、私の知識不足と実際に管理する処理の構築にかかるコストを考えた結果、速度計測をしてブログを書かせていただきました。
予想していた計測結果とは違いましたが、実際にサンプルを作って計測してみないことにはわからないという知見を得られました。

メモリの管理について勉強する際、配置newを活用してみるのも良いかと思います。
最後までご清覧いただき、ありがとうございました。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。