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つで動作を試します。
- 普通(C++98)
- 普通(C++11)
- newのメモリ確保
- スマートポインタを使ってメモリ確保
$ 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ライブラリ内でメモリ拡張時にコピーコンストラクタが無駄に起動して遅くなることがある。
std::vectorの中で例外を出されると保持している値が不安定になる事からnoexcept修飾子をムーブコンストラクタにつけなければコピーコンストラクタが呼ばれるらしいですよ。
返信削除