3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

C++で動的なサイズの多次元配列を扱いたい。

Last updated at Posted at 2022-01-29

最近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_newunique1より遅いのは不思議ですね。素直にmake_uniqueを使うべきなんでしょう。
ところで3次元unique_ptrをnewで確保するバージョンがないのは、書き方が解らず挫折したからです。4

初投稿時の測定結果 こちらの内容は測定条件が間違っていたので修正します。 vector: 8596 unique_ptr: 8729 array & new: 3488 template class: 3808 ~~それにしても、先ほども言いましたが、多次元配列でunique_ptrを使おうとすると、vectorより遅くなるんですね……。~~
  1. ところで unique_ptrの多次元配列作成の方法ですが、ググっても固定長の事例しか見つからなかったため、なかなか苦労しました。

  2. 道理で事例が見つからないわけだよ!

  3. 勉強になったのでヨシ!

  4. 誰か教えてください。

3
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?