3
5

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 5 years have passed since last update.

std::moveにまつわる速度調査など stringとラムダ編

Last updated at Posted at 2017-03-27

間違えている部分あると思うので
マサカリおねがいします!

そういえば ユニバーサル参照の説明してないわ、へんにstd::moveしてるわ、std::forwardしてないわ
説明として圧倒的に不足しているので
調査しなおして書き直します・・

はじめに

この調査を行った経緯は、強い人が書いたコードの中に1つ下記の疑問コードがあったため
std::moveや右辺値参照を7割ほどしか理解していなかったため、具体的な速度調査を行ってみた

まず、前提として右辺値やstd::moveをある程度知っている必要があり、そのへんは先人たちが分かりやすい説明しているので
そっちで見ていただきたい
要約すると 右辺値は一時オブジェクト。アドレスが取れるかどうか 的な判断がわかりやすい
std::moveは、所有権を移動する関数ではなく、右辺値型へのキャストと覚えた方がわかりやすい

また、C++14以降でラムダキャプチャが強化され、moveキャプチャ等も出来るらしいので、今のうちに 右辺値参照はPerfectに理解しておきたい

疑問コード

  hoge( std::move( [](){ほげほげ;} ) );

元々 このラムダは右辺値なのに std::moveする必要あるんだろうか?
(結論は 無駄だと思う)

上記疑問コードの調査の前に変数(ここではstd::string)を使って調査してみる

stringでの調査

まず例文

# include<iostream>
# include<chrono>

using namespace std;


template <typename Func>
 void time_elapsed(Func func, int count)
{
	auto t0 = std::chrono::high_resolution_clock::now();
	for (int n = 0; n < count; n++) {
		func();
	}
	auto t1 = std::chrono::high_resolution_clock::now();

	std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count() << " msec" << std::endl;
}

void foo(string s) {
	 string hoge = std::move(s);
}
void foo1(string &s) {
	 string hoge = std::move(s);
}
void foo2(string &&s) {
	 string hoge = std::move(s);
}

void bar(string s) {
	 string hoge = s;
}
void bar(string &s) {
	 string hoge = s;
}
void bar(string &&s) {
	 string hoge = s;
}

auto main() -> int{
	time_elapsed([] {  foo(string("helo"));  }, 1000000);  // 起動直後の計測は捨てる(ゴミデータ)

	time_elapsed([] {  foo(string("helo"));  }, 1000000);
	time_elapsed([] {  foo1(string("helo"));  }, 1000000);  VCだとなぜか通る
	time_elapsed([] {  foo2(string("helo"));  }, 1000000);

	cout << endl;

	time_elapsed([] {  foo(std::move(string("helo")));  }, 1000000);   // Warning
	time_elapsed([] {  foo1(std::move(string("helo")));  }, 1000000);  // VCだとなぜか通る
	time_elapsed([] {  foo2(std::move(string("helo")));  }, 1000000);  // Warning

	cout << endl;

	static string cc("hoge");  // あえてキャプチャせずStaticに
	time_elapsed([] {  foo(cc);  }, 1000000);
	time_elapsed([] {  foo1(cc);  }, 1000000);
//	time_elapsed([] {  foo2(cc);  }, 1000000);	// 当然コンパイルエラー

	cout << endl;

	time_elapsed([] {  foo(std::move(cc));  }, 1000000);
	time_elapsed([] {  foo1(std::move(cc));  }, 1000000);  // VCだとなぜか通る
	time_elapsed([] {  foo2(std::move(cc));  }, 1000000);


}

結果 
呼び出される関数でstd::move しない(foo
4961 msec
4958 msec
4948 msec

7249 msec
4964 msec
4952 msec

5315 msec
2675 msec

4889 msec
2533 msec
2528 msec

呼び出される関数でstd::move する(bar
4654 msec
4653 msec
4627 msec

6931 msec
4673 msec
4655 msec

4979 msec
2329 msec

4614 msec
2353 msec
2379 msec

Wandboxがスポンサーゲットしてたので 張っておく!
https://wandbox.org/permlink/2gIjhKZgR59EsjaJ

VisualStudioとそれ以外でコンパイルエラーになるケースが違うが、Clangでエラーやワーニングが出るコードは書くべきではないと思う

一時オブジェクト(右辺値)引数、std::move(一時オブジェクト)、変数(左辺値)、std::move(変数)
に対してそれぞれ、オブジェクト受け、参照受け(&)、右辺値参照受け(&&)
の、合計12パターンをテストしてみた

結果

上記をVisualStudioでビルドし、トレースおよびアセンブラで追っかけた結果、下記のパターンのグループが出来た

一時オブジェクト作成

4650ms
foo(string("helo")); foo1(string("helo")); foo2(string("helo"));
foo1(std::move(string("helo"))); foo2(std::move(string("helo")));

basic_string(_In_z_ const _Elem * const _Ptr)
foo
basic_string(_Myt&& _Right) _NOEXCEPT
_Assign_rv_contents_with_alloc_always_equal(_STD move(_Right), _Use_memcpy_move{});

foo1(string("helo"))、foo1(std::move(string("helo")))
右辺値の参照(not 右辺値参照)はillformだと思ってたけどVCだと通るんだね?? このあたり謎。

foo2(std::move(string("helo")))
一時オブジェクト(右辺値)をmoveするとWarning発生

一時オブジェクト作成、右辺値参照

7000ms
foo(std::move(string("helo")))

basic_string(_In_z_ const _Elem * const _Ptr)
basic_string(_Myt&& _Right) _NOEXCEPT
_Assign_rv_contents_with_alloc_always_equal(_STD move(_Right), _Use_memcpy_move{});
foo
basic_string(_Myt&& _Right) _NOEXCEPT
_Assign_rv_contents_with_alloc_always_equal(_STD move(_Right), _Use_memcpy_move{});

Warning発生。一時オブジェクトをmoveすべきではない。
実行速度もとても遅いので使ってはいけない

コピーコンストラクタ

4600ms
foo(cc)

basic_string(const _Myt& _Right)
_Assign_lv_contents(_Right);
foo
basic_string(_Myt&& _Right) _NOEXCEPT
_Assign_rv_contents_with_alloc_always_equal(_STD move(_Right), _Use_memcpy_move{});

foo(std::move(cc))

basic_string(_Myt&& _Right) _NOEXCEPT
_Assign_rv_contents_with_alloc_always_equal(_STD move(_Right), _Use_memcpy_move{});
foo
basic_string(const _Myt& _Right)
_Assign_lv_contents(_Right);

オブジェクトのコピーコンストラクトが行われるので当然おそい

コンパイルエラー

foo2(cc)

オブジェクトから右辺値参照は出来ない

処理なし

2330ms

foo1(cc)、foo1(std::move(cc))、foo2(std::move(cc))

foo
basic_string(_Myt&& _Right) _NOEXCEPT
_Assign_rv_contents_with_alloc_always_equal(_STD move(_Right), _Use_memcpy_move{});

通常の参照渡しも同じになるのは 受け関数でstd::moveしてるからのようだ
このコードが最も効率良いはずなので、常にこのコードをかきましょう!

おまけ

受け取り側の関数で std::moveを使わない場合は いずれのパターンも約300ms 遅くなった

void foo(string s) {
	 string hoge = s;
}

結論

まず VisualStudioだとコンパイル出来てしまうコードがあったのは驚いた(コンパイルオプションとかあるのかな?)
ただ いずれのパターンも書くべきでないしパフォーマンスも良くないので、正しい書き方をしましょう
私の考えでは 正解は1つじゃない。2つ。

コピー

速度とかじゃなく、コピーを渡す必要があるシーンは出てくる
関数内で値を書き換えるが、それは困るというとき
そういう時はコストかかってもコピーするしかない

hoge(string st){
  st +=" append string";
  std::cout << st << std::endl;
}
string foo("foo");
hoge(foo);

右辺値参照渡し

参照渡しと同じように扱え 速度も最も早い
右辺値も渡せるので、参照渡しでは対応できない問題も解決
完全転送も可能なので、コピーする必要がなければ、常にこれを使うべきである
左辺値を渡そうとするとエラーになるので右辺値にキャストする必要がある(それがstd::move)
右辺値参照ではあるが、仮引数の時は左辺値なので、関数内でstd::moveしなければ通常の参照と動作はかわらない

hoge(string &&st){
  string r(std::move(st));
  r +=" append string";
  std::cout << r << std::endl;
}

hoge(string("RV"));  // welform
string s("LV");
// hoge(s); // illform
 hoge(std::move(s)); // welform

もちろん、通常の参照時はオブジェクトの所有権を移動したくない場合は 関数をオーバーロードするとよい


template<typename Val>
void foo(Val &s) {
	 Val hoge(s);
     cout << "ref: " << hoge << endl;
}

template<typename Val>
void foo(Val &&s) {
	 Val hoge(std::move(s));
     cout << "rvref: " << hoge << endl;
}


auto main() -> int{
    string s("helo");
    foo(string("helo")); // rval
    foo(s);              // lval
    foo(std::move(s));   // rval
    
    string s2("hi");
    foo(string(s2));     // rval
}

いよいよラムダ

一言でいうと、関数(オブジェクト)も 一級オブジェクトなので、同じです。

コード


# include<iostream>
# include<chrono>

using namespace std;


template <typename Func>
 void time_elapsed(Func func, int count)
{
	auto t0 = std::chrono::high_resolution_clock::now();
	for (int n = 0; n < count; n++) {
		func();
	}
	auto t1 = std::chrono::high_resolution_clock::now();

	std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count() << " msec" << std::endl;
}


 template <typename Func>
 void foo(Func rmd) {
	 rmd();
}

 template <typename Func>
 void foo1(Func& rmd) {
	 rmd();
 }
 template <typename Func>
 void foo2(Func&& rmd) {
	 rmd();
 }


auto main() -> int{
	time_elapsed([] {  foo([] {string s("abc"); });  }, 1000000);  // 起動直後の計測は捨てる(ゴミデータ)

	time_elapsed([] {  foo ([] {});  }, 10000000); 
	time_elapsed([] {  foo1([] {});  }, 10000000);  // VCではなぜかとおる
	time_elapsed([] {  foo2([] {});  }, 10000000); 

	
	time_elapsed([] {  foo (std::move([] {}));  }, 10000000);  // Warning
	time_elapsed([] {  foo1(std::move([] {}));  }, 10000000); // VCでは通る
	time_elapsed([] {  foo2(std::move([] {}));  }, 10000000);  // Warning


}

結果(ローカル)
63 msec
52 msec
53 msec
70 msec
71 msec
70 msec

備考として foo側で std::move()を使いアクセス std::move(rmd)(); で下記の値
71 msec
69 msec
70 msec
88 msec
88 msec
86 msec

Wandbox
https://wandbox.org/permlink/LYAKIspu91nJtQv8

考察

一級オブジェクトなので、ラムダ関数だろうが、右辺値(前回の string("hoge"))と同じなことはわかっている
が、オブジェクトに比べてほとんど差がない
ラムダを一度変数に入れ static auto rm = []{}; それで渡しても ほぼ同じ結果であった
ラムダの場合は 関数ポインタが渡される程度なんだろうか(ここは検証出来てないししたくない)

結論

ラムダはコピーしても参照しても右辺値参照でも速度に差がなかった
VCだと微妙に std::moveすると遅くなったがclangでは差がなかったので
速度に関しては、考慮する必要がない

正解はやはりオブジェクトと同じ

ラムダを呼び出し元でも使いまわすのならコピーする
参照を移しても問題なければ右辺値参照で渡せばいい
完全転送も可能なので、出来る限り右辺値参照を使うべきである

3
5
0

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
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?