2014年2月1日土曜日

C++11のムーブコンストラクタの速度を試す

もうC++14が出ると言う話なので今更ですが、C++の2011年バージョン(C++11)のムーブコンストラクタ(右辺値参照)についてです。

C++11ではC++の機能アップが色々とありましたが、特にムーブコンストラクタ(右辺値参照)はSTLを使っていれば何もプログラムを変更せずに速度向上が見込めますし、素晴らしいです。

ムーブコンストラクタ(右辺値参照)のテスト用に以下のrvalue.cppを作成してみました。動作を切り替えるためのマクロが多く、見にくいプログラムですみません。

#include <stdio.h>
#include <string.h>
#include <vector>
#include <memory>

/**
 * 数値を文字列で持つクラス
 * コンストラクタで数値を指定する
 */
class NumStr {
private:
        int capa_;
        char *str_;
public:
        explicit NumStr(int n) {
                capa_ = 16;
                str_ = new char[capa_];
                sprintf(str_, "%d", n);
        }

        ~NumStr() {
                delete[] str_;
        }

        NumStr(const NumStr& ns) {
                capa_ = ns.capa_;
                str_ = new char[capa_];
                strcpy(str_, ns.str_);
                #ifdef PRINT
                printf("copy1 %s\n", str_);
                #endif
        }

        NumStr& operator=(const NumStr& ns) {
                 capa_ = ns.capa_;
                 str_ = new char[capa_];
                strcpy(str_, ns.str_);
                return *this;
        }

// C++11からの新機能 ムーブコンストラクタ
#if __cplusplus >= 201103L
        NumStr(NumStr&& ns) {
                str_ = ns.str_;
                ns.str_ = nullptr;
                #ifdef PRINT
                printf("move1 %s\n", str_);
                #endif
        }

        NumStr& operator=(NumStr&& ns) {
                str_ = ns.str_;
                ns.str_ = nullptr;
                return *this;
        }
#endif
};

/**
 * メイン
 */
int main()
{

// 普通にNumStrを使う
#if !(defined(SHARED_PTR)||defined(PTR))

        std::vector<NumStr> list;
        #ifdef RESERVE
        list.reserve(9999999 * 2 + 1);
        #endif
        for (int i = -9999999; i < 9999999; i++) {
                list.push_back(NumStr(i));
        }
#endif

// NumStrをnewでメモリ確保する
#ifdef PTR
        std::vector<NumStr*> list;

        #ifdef RESERVE
        list.reserve(9999999 * 2 + 1);
        #endif
        for (int i = -9999999; i < 9999999; i++) {
                list.push_back(new NumStr(i));
        }
        // メモリの解放が必要
        for (std::vector<NumStr*>::iterator i = list.begin();
                                        i < list.end(); i++) {
                delete (*i);
        }
#endif

// NumStrをnewでメモリ確保。shared_ptrで自動でメモリ解放
#ifdef SHARED_PTR
        std::vector<std::shared_ptr<NumStr> > list;

        #ifdef RESERVE
        list.reserve(9999999 * 2 + 1);
        #endif
        for (int i = -9999999; i < 9999999; i++) {
                list.push_back(std::make_shared<NumStr>(i));
        }
#endif
        return 0;
}

ムーブコンストラクタの動きが分かりやすいようにstd::stringクラスの代わりとして、NumStrクラスを作成しています。16バイトサイズ固定で、数値を文字列して持つ、簡単なクラスです。

そして、そのクラスで作成した文字列をstd:vector(配列のクラス) に保存するだけです。

配列に保存する方法として、以下の3つで動作を試します。
  1. 普通(C++98)
  2. 普通(C++11)
  3. newのメモリ確保
  4. スマートポインタを使ってメモリ確保
速度比較の結果は以下です(上記の順番です。環境はFedora 20、gcc 4.8.2です。)

$ g++ -std=c++98 -O2 rvalue.cpp
$ time ./a.out

real 0m4.835s
user 0m4.283s
sys 0m0.478s
$ g++ -std=c++11 -O2 rvalue.cpp
$ time ./a.out

real 0m4.062s
user 0m3.665s
sys 0m0.354s
$ g++ -std=c++11 -DPTR -O2 rvalue.cpp
$ time ./a.out

real 0m3.512s
user 0m3.112s
sys 0m0.386s
$ g++ -std=c++11 -DSHARED_PTR -O2 rvalue.cpp
$ time ./a.out

real 0m3.968s
user 0m3.334s
sys 0m0.590s

1番は以前のバージョンC++98で動作させたもの。遅いです。
2番はmain()側は同じですが、NumStrクラスではC++11の新しいムーブコンストラクタを使った状態になります。速くなっています。
以前のバージョンでは速度を速くしよう考えるとnewでメモリを取得して、ポインタを配列に入れる必要がありました。それが3番です。しかしメモリの解放が必要であり、管理が大変です。
そこで、メモリを自動解放するスマートポインタを使ったのが4番です。

2番は原理上もっと速くてもいいはず。そこで、調べてみると・・・

$ g++ -std=c++11 -DPRINT -O2 rvalue.cpp
$ ./a.out | head -n20
move1 -9999999
move1 -9999998
copy1 -9999999
move1 -9999997
copy1 -9999999
copy1 -9999998
move1 -9999996
move1 -9999995
copy1 -9999999
copy1 -9999998
copy1 -9999997
copy1 -9999996
move1 -9999994
move1 -9999993
move1 -9999992
move1 -9999991
copy1 -9999999
copy1 -9999998
copy1 -9999997
copy1 -9999996

どうも配列のメモリサイズを拡張するときに、コピーコンストラクタが動作して遅くなっているようでした。ムーブコンストラクタでも問題ないはずですけど、なぜコピーコンストラクタが動作するのだろう?

そこで、メモリサイズ拡張が一回で済むように、予めメモリ確保をするようにしてみました。

$ g++ -std=c++98 -DRESERVE -O2 rvalue.cpp
$ time ./a.out

real 0m3.198s
user 0m2.977s
sys 0m0.210s
$ g++ -std=c++11 -DRESERVE -O2 rvalue.cpp
$ time ./a.out

real 0m2.581s
user 0m2.369s
sys 0m0.203s
$ g++ -std=c++11 -DRESERVE -DPTR -O2 rvalue.cpp
$ time ./a.out

real 0m3.356s
user 0m3.011s
sys 0m0.335s
$ g++ -std=c++11 -DRESERVE -DSHARED_PTR -O2 rvalue.cpp
$ time ./a.out

real 0m3.591s
user 0m3.072s
sys 0m0.507s

2番のムーブコンストラクタを使ったものが一番速くなりました。

最後のまとめです。
  • ムーブコンストラクタ(右辺値参照)は速い!
  • 無理にnewを使わなくても良くなり、プログラムが作りやすい!
  • しかしstd::vectorライブラリ内でメモリ拡張時にコピーコンストラクタが無駄に起動して遅くなることがある。

1 件のコメント:

  1. std::vectorの中で例外を出されると保持している値が不安定になる事からnoexcept修飾子をムーブコンストラクタにつけなければコピーコンストラクタが呼ばれるらしいですよ。

    返信削除