1. ホーム
  2. c++

[解決済み】モダンC++はタダで性能を手に入れられる?

2022-04-11 12:01:18

質問内容

C++11/14は、単にC++98のコードをコンパイルするだけでも、パフォーマンスが上がると主張されることがあります。その正当性は通常、移動セマンティクスに沿ったもので、いくつかのケースでは rvalue コンストラクタが自動的に生成されたり、今では STL の一部になっているためです。このようなケースは、実は以前はRVOや同様のコンパイラ最適化によってすでに処理されていたのではないか、と私は考えています。

そこで質問なのですが、新しい言語機能をサポートするコンパイラーを使用して、修正なしでより速く実行できる C++98 コードの実際の例を教えてください。標準に準拠したコンパイラでは、コピー消去を行う必要はなく、移動セマンティクスが高速化をもたらすかもしれないことは理解していますが、病的でないケースを見てみたいのです。

EDIT: 念のため、私は新しいコンパイラが古いコンパイラより速いかどうかを尋ねているのではなく、私のコンパイラのフラグに -std=c++14 を追加すると、より速く実行できるコードがあるかどうか(コピーを避ける、しかし移動セマンティクス以外に何か思いつくなら、私も興味がある)です。

解決方法は?

C++03コンパイラをC++11として再コンパイルすると、実装の品質とは実質的に無関係に、無制限にパフォーマンスが向上する5つの一般的なカテゴリを私は知っています。 これらはすべて、移動セマンティクスのバリエーションです。

std::vector リロケート

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

を実行するたびに foo のバッファが再割り当てされると、C++03 では、すべての vectorbar .

C++11では、その代わりに bar::data のように、基本的に自由です。

今回の場合、これは内部での最適化に依存しています。 std コンテナ vector . 以下のすべてのケースで std コンテナは、C++オブジェクトであるため、効率的な move のセマンティクスは、コンパイラをアップグレードすると、C++11で "自動的に"されます。 を含む、それをブロックしないオブジェクト。 std コンテナもまた、自動的に改善された move のコンストラクタを使用します。

NRVOの失敗

NRVO(名前付き戻り値最適化)が失敗すると、C++03ではコピーに、C++11では移動に戻る。 NRVOの失敗は簡単です。

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

とかでもいい。

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

3つの値 -- 返り値、そして関数内の2つの異なる値 -- があります。 エリジョンによって、関数内の値は戻り値に「マージ」されますが、互いにマージされることはありません。 エリジオンは、関数内の値を戻り値に「マージ」することができますが、互いにマージすることはできません。

基本的な問題は、NRVOエリシオンは壊れやすいということです。 return のサイトで突然、診断が出ずにその場所でパフォーマンスが大幅に低下することがあります。 NRVOが失敗するほとんどのケースで、C++11の場合は move C++03はコピーで終わりますが。

関数の引数を返す

ここでもエリシオンは不可能です。

std::set<int> func(std::set<int> in){
  return in;
}

C++11では、これは安価です:C++03では、コピーを回避する方法はありません。 関数への引数は、パラメータと戻り値の寿命と位置が呼び出し側のコードで管理されるため、戻り値でエライ目に遭うことはないのです。

しかし、C++11では、一方から他方へ移行することができます。 (あまりおもちゃではない例では、何かが set ).

push_back または insert

しかし、C++11 では、rvalue move insert 演算子をオーバーロードすることで、コピーを節約することができます。

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

C++03では、一時的な whatever が作成され、それがベクターにコピーされます。 v . 2 std::string バッファが割り当てられ、それぞれ同じデータが割り当てられ、1つは捨てられる。

C++11では、一時的な whatever が作成されます。 その whatever&& push_back をオーバーロードすると move は、その一時的なものをベクターに v . 一つ std::string バッファが確保され、ベクターに移動されます。 空の std::string は破棄されます。

課題

以下の@Jarod42さんの回答から盗用。

エリシオンはassignで発生しないが、move-fromは発生する。

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

こちら some_function はエライドの候補を返しますが、オブジェクトを直接構築するために使用されていないため、エライドすることはできません。 C++03では、上記の結果、テンポラリーの内容が some_value . C++11では、それは some_value 基本的に自由です。


上記の効果を十分に発揮させるためには、ムーブコンストラクタと代入を合成してくれるコンパイラが必要です。

MSVC 2013 は、移動コンストラクタを std コンテナで使用されますが、型に対する移動コンストラクタは合成されません。

そのため std::vector などはMSVC2013では改善されませんが、MSVC2015から改善される予定です。

clang と gcc はずっと以前から暗黙の移動コンストラクタを実装しています。 Intel の 2013 年版コンパイラーは、暗黙的な移動コンストラクターの生成をサポートします。 -Qoption,cpp,--gen_move_operations (MSVC2013とのクロスコンパチビリティのために、デフォルトではそうなっていません)。