C++ での動的メモリ確保: new, malloc(), スマートポインタ等の使い分け

※本記事の内容はあくまで「目安」で、厳密な比較はしていません。ご了承ください。
※コンパイラや実際のコードによって結果が異なる可能性があります。
※本記事で言っている「安全」は、「ソースコードに人為的ミスがあったとしてもメモリリークを起こさない」という意味で書いています。

# 0. まとめ

使い分けとしては

- 速度よりも「安全」性が重要の場合
    - `std::vector` 等のコンテナ
        - `std::vector::resize()` の速度は `realloc()` よりも速い
        - 他にも `std::array` 等の便利なコンテナがある
    - `std::make_unique` 等のスマートポインタのヘルパ関数
- 「安全」性よりもメモリ確保時の速度の方が重要の場合、または同等に重要の場合
    - コンパイラの最適化を信用するなら `std::unique_ptr` & `new`
    - コンパイラの最適化を信用せず、メモリサイズを変更しないなら `new`
    - コンパイラの最適化を信用せず、メモリサイズを変更するなら `malloc()`
        - ただし、メモリサイズ変更の速度は最適化によらず `std::vector` よりも遅い

のようになるかと思います。

特徴は以下の通り:

- C++ の `new` は Java 等と異なり、プリミティブ型でも使用できる
- `new` よりスマートポインタの方が「安全」 (メモリリークが発生しにくい)
- スマートポインタより `new` の方が高速または同じ速度
    - (コンパイラの最適化により、同じ速度になる可能性があります。)
- `malloc()` と `new` はほぼ同じ速度
    - (「`new` は関数呼び出しでないから `new` の方が速い」という説明を見かけたのですが、ここでは確認できませんでした。)
- `malloc()` / `free()` ではクラスを扱った場合にコンストラクタ・デストラクタが呼ばれない
- `std::make_unique` や `std::vector` 等のコンテナは、`new` や `malloc()` より遅い
    - (コンパイラの最適化により、実行時間を少し改善できる可能性があります。)
- `malloc()` で確保したメモリは `realloc()` でサイズを変更できる
    - 仕様上、メモリ位置の変更と値のコピーが発生する可能性あり
- `std::vector` は配列サイズを変更できる
    - 仕様上、少なくとも追加分の値のコピーが発生する
    - 仕様上、既存の値に関して、メモリ位置の変更と値のコピーが発生するかは未定義？
- `new` で確保した配列はサイズを変更できない

参考「[malloc/free - ゼロから学ぶ C++](https://rinatz.github.io/cpp-book/appendix-malloc-and-free/)」
参考「[Unterschied zwischen new und malloc ()](https://gadget-info.com/difference-between-new)」

# 1. メモリの確保・解放の速度

厳密な比較にはなっていないかもしれませんが、おおよそ

- 最適化ありの場合: `new` = `malloc()` = (`std::unique_ptr` & `new`) << `std::make_unique` = `std::vector`
- 最適化なしの場合: `new` = `malloc()` < (`std::unique_ptr` & `new`) << `std::make_unique` << `std::vector`

のような順の実行時間になりました。

※ものによっては確実に解放されるとは限らない仕様になっているため、厳密な比較にはなっていません。

## 1.1. テスト用コード

```C++:main.cpp
#include <iostream>
#include <memory>
#include <fcntl.h>
#include <chrono>
#include <stdlib.h>
#include <vector>

#define N (1024 * 1024)
#define N2 10

// 
void printTimeOfMakedUniquePtr();
void printTimeOfUniquePtr();
void printTimeOfVector();
void printTimeOfNew();
void printTimeOfMAlloc();

// 
int wmain() {

	// 出力の文字コード指定
	_setmode(fileno(stdout), _O_U8TEXT);
	_setmode(fileno(stderr), _O_U8TEXT);

	// 
	for (int i = 0; i < N2; i++) {
		printTimeOfMakedUniquePtr();
		printTimeOfUniquePtr();
		printTimeOfVector();
		printTimeOfNew();
		printTimeOfMAlloc();
		std::wcout << L"\n";
	}

	std::wcout << std::flush;

	return 0;

}

void printTimeOfMakedUniquePtr() {

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

	for (int i = 0; i < N; i++) {
		auto array = std::make_unique<int []>(1024);
		array = nullptr;
	}

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

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}

void printTimeOfUniquePtr() {

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

	for (int i = 0; i < N; i++) {
		auto array = std::unique_ptr<int []>(new int[1024]);
		array = nullptr;
	}

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

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}

void printTimeOfVector() {

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

	for (int i = 0; i < N; i++) {
		std::vector<int> v(1024);
		v.clear();
		v.shrink_to_fit();
	}

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

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}

void printTimeOfNew() {

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

	for (int i = 0; i < N; i++) {
		int * const array = new int[1024];
		delete[] array;
	}

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

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}

void printTimeOfMAlloc() {

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

	for (int i = 0; i < N; i++) {
		int * const array = (int *) malloc(sizeof(int) * 1024);
		if ( array == NULL ) {
			std::wcerr << L"Error: malloc()" << std::endl;
			break;
		}
		free(array);
	}

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

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}
```

## 1.2. 結果

- `std::make_unique`
- `std::unique_ptr` & `new`
- `std::vector`
- `new`
- `malloc()`

と、それぞれの解放処理を含めた各実行時間:

```text:出力&#058;&nbsp;最適化なし&nbsp;-O0
878     151     2905    82      77
857     149     2907    81      78
853     151     2911    80      82
849     154     2914    86      77
857     148     2907    82      78
855     150     2908    81      78
854     150     2909    82      78
854     149     2914    82      76
854     149     2904    79      76
851     150     2904    81      78
```

```text:出力&#058;&nbsp;最適化&nbsp;-O2
480     86      492     88      89
488     87      484     86      84
490     88      483     88      92
492     87      487     90      85
482     87      486     91      86
489     89      485     87      85
487     86      486     86      92
482     85      486     87      86
481     87      494     91      86
497     86      485     86      85
```

```text:出力&#058;&nbsp;最適化&nbsp;-O3&nbsp;-mtune=native&nbsp;-march=native
207     81      211     80      80
209     82      206     80      79
207     81      207     84      76
206     80      212     80      76
208     81      208     79      80
207     80      207     82      77
206     81      211     82      77
210     80      205     81      80
207     81      209     80      78
207     80      212     80      77
```

# 2. `std::vector::resize()` と `realloc()` の速度比較

具体的な値などの場合によるかもしれませんが、ここでは `std::vector::resize()` の方が明らかに高速でした。

## 2.1. テスト用コード

```C++:main.cpp
#include <iostream>
#include <fcntl.h>
#include <chrono>
#include <stdlib.h>
#include <vector>

#define N (1024 * 100)
#define N2 10

// 
void printTimeOfVectorResize();
void printTimeOfVectorReAlloc();

// 
int wmain() {

	// 出力の文字コード指定
	_setmode(fileno(stdout), _O_U8TEXT);
	_setmode(fileno(stderr), _O_U8TEXT);

	// 
	for (int i = 0; i < N2; i++) {
		printTimeOfVectorResize();
		printTimeOfVectorReAlloc();
		std::wcout << L"\n";
	}

	std::wcout << std::flush;

	return 0;

}

void printTimeOfVectorResize() {

	std::vector<int> v(16);

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

	for (int i = 2; i < N; i++) {
		v.resize(i * 16);
		v[i * 16 - 1] = 23;
	}

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

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}

void printTimeOfVectorReAlloc() {

	int * array = (int *) malloc(sizeof(int) * 16);
	if ( array == NULL ) {
		std::wcerr << L"Error: malloc()" << std::endl;
		return;
	}

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

	for (int i = 2; i < N; i++) {
		int * const newArray = (int *) realloc(array, sizeof(int) * i * 16);
		if ( newArray == NULL ) {
			std::wcerr << L"Error: realloc()" << std::endl;
			break;
		}
		array = newArray;
		array[i * 16 - 1] = 23;
	}

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

	free(array);

	std::wcout << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << L"\t";

}
```

## 2.2. 結果

`std::vector::resize()` は最適化で高速化しましたが、`realloc()` の速度は変わりませんでした。

`std::vector::resize()` と `realloc()` の実行時間:

```text:出力&#058;&nbsp;最適化なし&nbsp;-O0
12      2000
13      2007
14      2005
13      1999
14      2024
15      2115
14      2088
13      2024
14      2129
15      2041
```

```text:出力&#058;&nbsp;最適化&nbsp;-O2
5       2056
6       2036
7       2050
6       2058
8       2048
5       2040
5       2041
5       2046
6       2036
7       2043
```

```text:出力&#058;&nbsp;最適化&nbsp;-O3&nbsp;-mtune=native&nbsp;-march=native
6       2084
6       2055
6       2061
5       2045
5       2054
5       2046
6       2041
6       2051
6       2049
5       2049
```

# 3. その他

## 3.1. メモリリークに注意

スマートポインタや `std::vector` 等のコンテナは (基本的には) 解放し忘れが起きないので「安全」です。

`new` / `delete` や `malloc()` / `free()` 等、解放処理を忘れないよう注意が必要です。
特に、同一のスコープ内でメモリを確保した後にエラーが発生したときに、`return` や `try` / `catch`する場合等に解放処理を忘れがちです (エラー発生時にプログラム本体を終了しない場合に注意) 。

`new` で確保した「**配列**」は、`delete[]` で解放する必要があります (配列を `delete` した場合の動作は仕様上未定義) 。

※ `std::unique_ptr` と `new` を使用した場合にメモリリークする可能性が理論上存在します。

参考「[c++ - make_uniqueの利点 - スタック・オーバーフロー](https://ja.stackoverflow.com/questions/24876/make-unique%E3%81%AE%E5%88%A9%E7%82%B9)」
(※そもそも関数の引数に複数 `new` を書くような書き方自体良くない気がしますが…。)

## 3.2. 可変長配列 (VLA) について

C++ の仕様としては非推奨ですが、gcc や g++ 等で可変長配列 (Variable Length Array) を使用することができます。

※確保するサイズを実行時に決定可能ですが、確保後にサイズを変更することはできません。

```C++:main.cpp
#include <iostream>
#include <fcntl.h>

// 
void printA(size_t size);

// 
int wmain() {

	// 出力の文字コード指定
	_setmode(fileno(stdout), _O_U8TEXT);

	// 
	printA(3);
	printA(5);

	std::wcout << std::flush;

	return 0;

}

void printA(size_t size) {

	wchar_t str[size + 1]; // ★

	std::fill_n(str, size, L'あ');

	str[size] = L'\0';

	std::wcout << str << L"\n";

}
```

```text:結果
あああ
あああああ
```

## 3.3. 本記事で使用したコンパイルオプション

```bash:最適化なし&nbsp;-O0&nbsp;の場合
x86_64-w64-mingw32-g++ -Wall \
	-finput-charset=UTF-8 -fexec-charset=CP932 \
	-municode \
	-static-libgcc -static-libstdc++ \
	-O0 \
	-o main.exe \
	main.cpp
```