最近C++始めました。
最近3次元の配列構造を扱う必要がありました。
ただ、サイズが大きいので、std::vector
を使うと初期化の時間が無駄じゃないのかというのが気になったので、思いついたやり方を幾つか試して時間計測してみました。
はじめに
もっと良い方法をご存じでしたら教えてください!
実行環境
- Windows10
- Microsoft Visual Studio Community 2019
- C言語標準: C++14
- CPU: AMD Ryzen 5 4500U (2.38GHz
- RAM: 8.00GB
試したのはVisual Studio 上ですが、Windows特有の機能は使っていないはずなので、gccでも動くかと思います。
試したパターン
std::vector
まずは素朴にvectorで書きます。
void use_vector() {
// Vectorによる初期化
auto v1(std::vector<std::vector<std::vector<int>>>(asize, std::vector<std::vector<int>>(bsize, std::vector<int>(csize))));
// 値の代入
for (auto a = 0; a < asize; a++)
for (auto b = 0; b < bsize; b++)
for (auto c = 0; c < csize; c++)
v1[a][b][c] = getvalue();
}
asize
, bsize
, csize
, は、各次元における配列長で、getvalue()
は何か値を返す関数です。(以下同様)
初期化の際に、配列全体の値が0で初期化されます。
std::unique_ptr(2022/1/30修正)
vector
版における0初期化の時間を削減したいとは思ったものの、new
を使うのは怖かったので、覚えたてのunique_ptr
を試してみました。1
void use_unique_ptr() {
// 領域確保
auto v2(std::make_unique<std::unique_ptr<std::unique_ptr<int[]>[]>[]>(asize));
for (auto a = 0; a < asize; a++) {
v2[a] = std::make_unique<std::unique_ptr<int[]>[]>(bsize);
for (auto b = 0; b < bsize; b++)
v2[a][b] = std::make_unique<int[]>(csize);
}
// 値の代入
for (auto a = 0; a < asize; a++)
{
// v2[a] = std::make_unique<std::unique_ptr<int[]>[]>(bsize); <-間違い(2022/1/30 削除)
for (auto b = 0; b < bsize; b++)
{
// v2[a][b] = std::make_unique<int[]>(csize); <-間違い(2022/1/30 削除)
for (auto c = 0; c < csize; c++)
v2[a][b][c] = getvalue();
}
}
}
で、結論から書くと、vector版と同じか、むしろ遅いくらいです。2
(2022/1/30追記)ごめんなさい。コードを見直したら、値の代入の時にまで無駄にmake_uniqueしてました!
ちゃんとVectorより速かったです。
一次元配列版(2022/1/30 追記)
むしろ配列を3重にせずに扱う方が楽なのでは?と思い立って追加実験。
void use_vector2() {
// 初期化
auto v2(std::vector<int>(asize * bsize * csize));
// 値の代入
for (auto a = 0; a < asize; a++)
for (auto b = 0; b < bsize; b++)
for (auto c = 0; c < csize; c++)
v2[(a * bsize + b) * csize + c] = getvalue();
}
void use_unique_ptr2() {
// 領域確保
auto v2(std::make_unique<int[]>(asize * bsize * csize));
// 値の代入
for (auto a = 0; a < asize; a++)
for (auto b = 0; b < bsize; b++)
for (auto c = 0; c < csize; c++)
v2[(a * bsize + b) * csize + c] = getvalue();
}
unique_ptrをnewで確保(2022/1/30 追記)
さらに、std::make_unique<int>()
で配列を確保した場合、実は0初期化が実行されるという情報を入手したので、newで確保してみます。
void use_unique_ptr3() {
// 領域確保
auto v2(new int[asize * bsize * csize]);
// 値の代入
for (auto a = 0; a < asize; a++)
for (auto b = 0; b < bsize; b++)
for (auto c = 0; c < csize; c++)
v2[(a * bsize + b) * csize + c] = getvalue();
}
newで配列を動的確保
new
なんか使いたくないといったな。あれは嘘だ……というわけでもないのですが、今回は実験ということでやってみました。
これ、あってるんですか?
void use_array() {
// 領域確保
int*** v2 = new int**[asize];
for (auto a = 0; a < asize; a++) {
v2[a] = new int*[bsize];
for (auto b = 0; b < bsize; b++)
v2[a][b] = new int[csize];
}
// 値の代入
for (auto a = 0; a < asize; a++)
for (auto b = 0; b < bsize; b++)
for (auto c = 0; c < csize; c++)
v2[a][b][c] = getvalue();
// 領域解放
for (auto a = 0; a < asize; a++) {
for (auto b = 0; b < bsize; b++)
delete[] v2[a][b];
delete[] v2[a];
}
delete[] v2;
}
この3つの中で一番早いです。
class化してnew&deleteを自動化
delete忘れは怖いと思ったので、unique_ptrと合わせて自動化できないかと試行錯誤していたら、いつの間にかこんな感じになっていました。
勉強しながらtemplate classを初めて使ってみましたが、この例だけならint型のままで良かったですね…。3
template <typename T>
class vector3d {
public:
vector3d(int asize, int bsize, int csize);
~vector3d();
T*** _data = nullptr;
private:
int _asize = 0;
int _bsize = 0;
int _csize = 0;
};
template <typename T>
vector3d<T>::vector3d(int asize, int bsize, int csize) :_asize(asize), _bsize(bsize), _csize(csize) {
// constructorで領域確保
_data = new T**[_asize];
for (auto l = 0; l < _asize; l++) {
_data[l] = new T*[_bsize];
for (auto b = 0; b < _bsize; b++)
_data[l][b] = new T[_csize];
}
}
template <typename T>
vector3d<T>::~vector3d() {
// destructorで領域解放
for (auto l = 0; l < _asize; l++) {
for (auto b = 0; b < _bsize; b++)
delete[] _data[l][b];
delete[] _data[l];
}
delete[] _data;
_data = nullptr;
// 呼び出し確認用の表示
// std::cout << "destructor is called!\n";
}
void use_vector3d_template_class() {
// 領域確保
auto v3(std::make_unique<vector3d<int>>(asize, bsize, csize));
// 値の代入
for (auto a = 0; a < asize; a++)
for (auto b = 0; b < bsize; b++)
for (auto c = 0; c < csize; c++)
v3->_data[a][b][c] = getvalue();
}
これなら不要になった際に自動でdestructorが呼ばれるため、delete[] 忘れはなくなります。とはいえいろいろ拙い部分もありますので、改善すべく勉強中です。
(3->_data
みたいな書き方も気になりますが、特に T*** _data
をpublicにするのがよろしくない気がします)
実行&結果
呼び出し部分のコード
上記のコードをまとめて、下記のような感じで呼び出します。
#include <iostream>
#include <vector>
#include <chrono>
auto asize = 200;
auto bsize = 200;
auto csize = 200;
auto getvalue() {
return 10;
}
// ~~中略~~
int main() {
auto limit = 200;
auto start = std::chrono::system_clock::now();
for (auto i = 0; i < limit; i++) use_vector();
std::cout << "vector3: ";
std::cout << (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start).count()) << std::endl;
// (以下省略) 他のバージョンも同様。
}
測定結果 (2022/1/30修正)
お待ちかねの測定結果がこちらです。(単位はミリ秒)
vector3: 8449 // 3次元vector
vector1: 3518 // 1次元vector
unique3: 5820 // 3次元unique_ptr
unique1: 3260 // 1次元unique_ptr & make_uniqueで確保
unique1_new: 4456 // 1次元unique_ptr & newで確保 (0初期化回避)
array & new: 3597 // 3次元配列をnewで確保
template class: 4113 // 3次元配列をnewで確保し、class化
今回は配列作成後の処理が軽かったのもありますが、結構な差になりました。
というわけで、配列サイズで速度が気になるようなら、newで確保しつつ、classにして隠蔽するのが良さそうです。
(2022/1/30追記)
いろいろやってみましたが、1次元vectorにするだけでここまで早くなるのはびっくりです。添え字計算用に補助関数はあった方が良いと思いますが、気になるのはそれぐらいです。
また、0初期化していないはずのunique1_new
がunique1
より遅いのは不思議ですね。素直にmake_uniqueを使うべきなんでしょう。
ところで3次元unique_ptrをnewで確保するバージョンがないのは、書き方が解らず挫折したからです。4