NRVO(Named Return Value Optimization)が適用されたか判定するマクロ
注: この記事は連休中にボケた頭を戻すため書いたものなので、あまり役には立つ記事ではありません。
C++で関数/メソッドでオブジェクトを返したい時がありますが、以下のようにオブジェクトを返す関数/メソッドでは、コピーにかかるコストが気になります。C言語をそれなりにやってきた人であれば、このように、(Tのサイズが大きいものだったとして)大きいデータを値渡しで返すようなことは普通しません。ただ、C++ではRVO(Return Value Optimization)という最適化が働き、呼び出し元(main)のt変数が直接初期化されるので、コピーは発生しません。このため、コピーコストを気にせずオブジェクトをどんどん返していけます。
RVOの例
T exampleRVO() { return T(); // 一時オブジェクトを返す } int main() { T t = exampleRVO(); }
RVOは最適化処理なので、実際に最適化されるかされないかはコンパイラ依存になりますが、上記のように一時オブジェクトを返すケースでは、よっぽど古いコンパイラでない限りは、まず最適化されるはずです。また、C++17以降ではこのようなケースでは最適化が必須となったようですので、安心してオブジェクトを返すことができます。
ただし、変数を返す場合のNRVO(Named Return Value Optimization)はC++17においても保証されません。
NRVOの例
T exampleNRVO() { T t; // named objectを生成 t.value = 2; return t; // named objectを返す } int main() { T t = exampleNRVO(); }
上記の例ではNRVOは適用されるはずですが、関数の処理内容によっては、NRVOが適用できないケースもあります。少し古い記事ですが、以下の記事にいくつか例があります。
https://docs.microsoft.com/en-us/previous-versions/ms364057(v=vs.80)#nrvo_cpp05_topic3
RVOを期待した処理を書きつつ実際には適用されていないと、オブジェクトの種類によってはパフォーマンス的に悲惨なことになるので、「この関数は本当にNRVOが効いているのか?」と不安になることがあります(他にそんな人はいないんですかね?)。
RVO/NRVOが適用されているかを確認するには、例えば以下の方法が考えられます。
- コピーコンストラクタを定義して、メッセージを挿入するなどして確認する
- 逆アセンブルして最適化の有無を確認する
ただ、return対象のオブジェクトがstd::string等標準ライブラリのクラスのように手を入れづらいケースもあったりして、最適化の確認が億劫になることもあります。
そこで、呼び出し元に返すインスタンスがスタック上のどこに生成されているかをチェックすることで、NRVOが適用されているかを確認できる数行のマクロを作ってみました。NRVOが適用されていないと考えられる場合はメッセージを出力します。
nrvo.cc
#include <iostream> // xにrbpレジスタの値を設定 #define CURRENT_RBP(x) \ __asm__("mov %%rbp, %0;" \ :"=r"(x) \ ); // RVOが適用されているかをチェックするマクロ #define CHECK_NRVO(tag, object) \ { \ unsigned long rbp; \ CURRENT_RBP(rbp); \ unsigned long address = reinterpret_cast<unsigned long>(&object); \ if (rbp > address) { \ std::cout << tag << ": NRVO isn't applied." << std::endl; \ } \ } class Something { public: int value; Something() : value(0) {} Something(const Something& obj) { std::cout << "copy constructor called\n"; value = obj.value; } }; // 名前付きのオブジェクトを返す関数 Something create_object() { Something object; CHECK_NRVO("object", object); return object; } // 条件によって異るオブジェクトを返す関数 Something create_object_by_condition(bool flag) { Something object1, object2; CHECK_NRVO("object1", object1); CHECK_NRVO("object2", object2); return flag ? object1 : object2; } int main() { Something object = create_object(); Something object2 = create_object_by_condition(true); }
CHECK_NRVOマクロはobjectのアドレスとrbpレジスタの大小関係を判定します。rbp > object address なら現関数のスタックフレーム上にオブジェクトが生成されているので、NRVOが適用されていないと考えられます。ただし、-O2等の最適化オプション指定があると関数の先頭でrbpが設定されないので正常に判定できません。
nrvo.ccの実行結果
object1: NRVO isn't applied. # create_object_by_condition()ではNRVOが効いていない object2: NRVO isn't applied. copy constructor called
このマクロが機能する条件は以下のとおりです。
- x86_64アーキテクチャー限定
- 試したコンパイラはg++,clang++
- -O2のように最適化オプションは指定しない(g++ -std=c++17 nrvo.cc のようにコンパイル)
- inline,constexpr関数は考慮しない
試すには最適化オプションを外して試す必要がありますが、未指定状態でNRVOが動作しているのであれば、最適化指定してそれが適用されなくなるということはないのではないかと思います。
これで、コピーコンストラクタを定義したり、逆アセンブルせずにNRVOの適用を確認できます。簡単にNRVOの適用を確認できて少し安心(?)できるかもしれません。
投稿日:2019/05/06 19:13
タグ: C++