間違えている部分あると思うので
マサカリおねがいします!
そういえば ユニバーサル参照の説明してないわ、へんに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では差がなかったので
速度に関しては、考慮する必要がない
正解はやはりオブジェクトと同じ
ラムダを呼び出し元でも使いまわすのならコピーする
参照を移しても問題なければ右辺値参照で渡せばいい
完全転送も可能なので、出来る限り右辺値参照を使うべきである