ショートバージョン:多くのプログラミング言語では、ベクトル/配列などの大きなオブジェクトを返すのが一般的です。クラスにムーブコンストラクターがある場合、このスタイルはC++ 0xで受け入れられるようになりましたか?
長いバージョン: C++ 0xでは、これはまだ悪い形と考えられていますか?
std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();
従来のバージョンは次のようになります。
void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);
新しいバージョンでは、BuildLargeVector
から返される値は右辺値であるため、(N)RVOが実行されないと仮定して、vはstd::vector
の移動コンストラクターを使用して構築されます。
C++ 0xより前でも、(N)RVOのために最初の形式はしばしば「効率的」でした。ただし、(N)RVOはコンパイラーの裁量です。右辺値参照ができたので、ディープコピーが行われないことはguaranteedです。
編集:質問は最適化に関するものではありません。示されている両方の形式は、実世界のプログラムでほぼ同じパフォーマンスを持っています。一方、以前は、最初のフォームのパフォーマンスが桁違いに劣っていた可能性があります。その結果、最初の形式は、長い間C++プログラミングの主要なコード臭でした。もうありません、私は願っていますか?
Dave Abrahamsには、 値を渡す/返す速度 のかなり包括的な分析があります。
簡単な答え、値を返す必要がある場合は値を返します。とにかくコンパイラがそれを行うので、出力参照を使用しないでください。もちろん警告がありますので、その記事を読んでください。
少なくともIMOは、通常は貧弱なアイデアですが、効率上の理由からnotです。問題の関数は通常、イテレータを介して出力を生成する汎用アルゴリズムとして記述される必要があるため、これは不適切なアイデアです。反復子を操作する代わりにコンテナを受け入れるまたは返すコードは、ほとんど疑わしいと見なされます。
誤解しないでください:コレクションのようなオブジェクト(文字列など)を渡すのが理にかなっている場合がありますが、引用した例では、ベクトルを渡すか返すことは悪い考えだと思います。
要点は次のとおりです。
エリシオンとRVOのコピーcan「怖いコピー」を避けます(コンパイラーはこれらの最適化を実装する必要はなく、状況によっては適用できません)
C++ 0x RValue参照allow文字列/ベクターの実装guarantees that。
古いコンパイラ/ STL実装を放棄できる場合は、ベクトルを自由に返します(そして、独自のオブジェクトもそれをサポートしていることを確認してください)。コードベースが「より少ない」コンパイラをサポートする必要がある場合は、古いスタイルに固執します。
残念ながら、それはインターフェイスに大きな影響を及ぼします。 C++ 0xがオプションではなく、保証が必要な場合は、いくつかのシナリオで代わりに参照カウントオブジェクトまたはコピーオンライトオブジェクトを使用することがあります。ただし、マルチスレッドには欠点があります。
(C++での1つの答えが単純で簡単で、条件がなければいいのですが)。
実際、C++ 11以降、copying the _std::vector
_のコストはほとんどの場合なくなっています。
ただし、constructing new vector(then destructing it)のコストは依然として存在し、値で返す代わりに出力パラメーターを使用することは依然として有用であることに留意してくださいベクターの容量を再利用したい場合。これは、C++コアガイドラインの F.2 の例外として文書化されています。
比較してみましょう:
_std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
_
で:
_void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
_
ここで、これらのメソッドをタイトループでnumIter
回呼び出し、何らかのアクションを実行する必要があるとします。たとえば、すべての要素の合計を計算してみましょう。
_BuildLargeVector1
_を使用すると、次のようになります。
_size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
_
_BuildLargeVector2
_を使用すると、次のようになります。
_size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
_
最初の例では、多くの不必要な動的割り当て/割り当て解除が発生しますが、2番目の例では、出力パラメーターを古い方法で使用して、既に割り当てられたメモリを再利用することで防止します。この最適化を行う価値があるかどうかは、値の計算/変更のコストと比較した割り当て/割り当て解除の相対的なコストに依存します。
vecSize
とnumIter
の値で遊んでみましょう。 vecSize * numIterを一定に保ち、「理論上」同じ時間がかかるようにします(=同じ数の割り当てと追加が同じ数であり、まったく同じ値を持ちます)。時間の差は、割り当て、割り当て解除、およびキャッシュのより良い使用。
より具体的には、vecSize * numIter = 2 ^ 31 = 2147483648を使用します。これは、16GBのRAMであり、この数値により8GBを超えないように割り当てられるためです(sizeof(int)= 4)、ディスクにスワップしていないことを確認します(他のすべてのプログラムは閉じられていて、テストの実行時に〜15GBが使用可能でした)。
コードは次のとおりです。
_#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>
class Timer {
using clock = std::chrono::steady_clock;
using seconds = std::chrono::duration<double>;
clock::time_point t_;
public:
void tic() { t_ = clock::now(); }
double toc() const { return seconds(clock::now() - t_).count(); }
};
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
int main() {
Timer t;
size_t vecSize = size_t(1) << 31;
size_t numIter = 1;
std::cout << std::setw(10) << "vecSize" << ", "
<< std::setw(10) << "numIter" << ", "
<< std::setw(10) << "time1" << ", "
<< std::setw(10) << "time2" << ", "
<< std::setw(10) << "sum1" << ", "
<< std::setw(10) << "sum2" << "\n";
while (vecSize > 0) {
t.tic();
size_t sum1 = 0;
{
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
}
double time1 = t.toc();
t.tic();
size_t sum2 = 0;
{
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
} // deallocate v
double time2 = t.toc();
std::cout << std::setw(10) << vecSize << ", "
<< std::setw(10) << numIter << ", "
<< std::setw(10) << std::fixed << time1 << ", "
<< std::setw(10) << std::fixed << time2 << ", "
<< std::setw(10) << sum1 << ", "
<< std::setw(10) << sum2 << "\n";
vecSize /= 2;
numIter *= 2;
}
return 0;
}
_
結果は次のとおりです。
_$ g++ -std=c++11 -O3 main.cpp && ./a.out
vecSize, numIter, time1, time2, sum1, sum2
2147483648, 1, 2.360384, 2.356355, 2147483648, 2147483648
1073741824, 2, 2.365807, 1.732609, 2147483648, 2147483648
536870912, 4, 2.373231, 1.420104, 2147483648, 2147483648
268435456, 8, 2.383480, 1.261789, 2147483648, 2147483648
134217728, 16, 2.395904, 1.179340, 2147483648, 2147483648
67108864, 32, 2.408513, 1.131662, 2147483648, 2147483648
33554432, 64, 2.416114, 1.097719, 2147483648, 2147483648
16777216, 128, 2.431061, 1.060238, 2147483648, 2147483648
8388608, 256, 2.448200, 0.998743, 2147483648, 2147483648
4194304, 512, 0.884540, 0.875196, 2147483648, 2147483648
2097152, 1024, 0.712911, 0.716124, 2147483648, 2147483648
1048576, 2048, 0.552157, 0.603028, 2147483648, 2147483648
524288, 4096, 0.549749, 0.602881, 2147483648, 2147483648
262144, 8192, 0.547767, 0.604248, 2147483648, 2147483648
131072, 16384, 0.537548, 0.603802, 2147483648, 2147483648
65536, 32768, 0.524037, 0.600768, 2147483648, 2147483648
32768, 65536, 0.526727, 0.598521, 2147483648, 2147483648
16384, 131072, 0.515227, 0.599254, 2147483648, 2147483648
8192, 262144, 0.540541, 0.600642, 2147483648, 2147483648
4096, 524288, 0.495638, 0.603396, 2147483648, 2147483648
2048, 1048576, 0.512905, 0.609594, 2147483648, 2147483648
1024, 2097152, 0.548257, 0.622393, 2147483648, 2147483648
512, 4194304, 0.616906, 0.647442, 2147483648, 2147483648
256, 8388608, 0.571628, 0.629563, 2147483648, 2147483648
128, 16777216, 0.846666, 0.657051, 2147483648, 2147483648
64, 33554432, 0.853286, 0.724897, 2147483648, 2147483648
32, 67108864, 1.232520, 0.851337, 2147483648, 2147483648
16, 134217728, 1.982755, 1.079628, 2147483648, 2147483648
8, 268435456, 3.483588, 1.673199, 2147483648, 2147483648
4, 536870912, 5.724022, 2.150334, 2147483648, 2147483648
2, 1073741824, 10.285453, 3.583777, 2147483648, 2147483648
1, 2147483648, 20.552860, 6.214054, 2147483648, 2147483648
_
(Intel i7-7700K @ 4.20GHz; 16GB DDR4 2400Mhz; Kubuntu 18.04)
表記:mem(v)= v.size()* sizeof(int)= v.size()* 4私のプラットフォームで。
当然のことながら、_numIter = 1
_(つまり、mem(v)= 8GB)の場合、時間は完全に同じです。実際、どちらの場合も、メモリ内に8GBの巨大なベクトルを一度だけ割り当てています。これは、BuildLargeVector1()の使用時にコピーが発生しなかったことも証明します。コピーを行うのに十分なRAMがありません!
_numIter = 2
_の場合、2番目のベクトルを再割り当てする代わりにベクトル容量を再利用すると、1.37倍高速になります。
_numIter = 256
_の場合、ベクトル容量を再利用する(256回繰り返してベクトルを割り当て/割り当て解除する代わりに...)は、2.45倍高速です:)
Time1は_numIter = 1
_から_numIter = 256
_までほぼ一定であることがわかります。つまり、8GBの1つの巨大なベクトルを割り当てると、32MBの256個のベクトルを割り当てるのと同じくらいのコストがかかります。ただし、8GBの1つの巨大なベクトルを割り当てることは、32MBの1つのベクトルを割り当てるよりも明らかに高価です。そのため、ベクトルの容量を再利用するとパフォーマンスが向上します。
_numIter = 512
_(mem(v)= 16MB)から_numIter = 8M
_(mem(v)= 1kB)までがスイートスポットです。両方のメソッドは、numIterとvecSizeの他のすべての組み合わせとまったく同じくらい高速で高速です。 。これはおそらく、プロセッサのL3キャッシュサイズが8MBであるため、ベクターがキャッシュにほぼ完全に収まるようになっているためと思われます。 _time1
_の突然のジャンプがmem(v)= 16MBである理由については本当に説明していませんが、mem(v)= 8MBの直後に発生するのはより論理的に思えます。驚くことに、このスイートスポットでは、容量の再利用は実際にはわずかに速いことに注意してください。私はこれを本当に説明しません。
_numIter > 8M
_がthingsいものになり始めると。どちらの方法も遅くなりますが、値によるベクトルの返送はさらに遅くなります。最悪の場合、単一のint
のみを含むベクターでは、値で返す代わりに容量を再利用するのが3.3倍速くなります。おそらく、これは支配的になり始めるmalloc()の固定費によるものです。
Time2の曲線がtime1の曲線よりも滑らかであることに注意してください。ベクトル容量の再利用が一般に高速であるだけでなく、おそらくもっと重要なことは、より多くの予測可能です。
また、スイートスポットでは、約0.5秒で64ビット整数の20億の加算を実行できたことに注意してください。これは、4.2Ghz 64ビットプロセッサで非常に最適です。 8つのコアすべてを使用するために計算を並列化することで、より良い結果を得ることができます(上記のテストでは、一度に1つのコアのみを使用します。 mem(v)= 16kBのときに最高のパフォーマンスが得られます。これはL1キャッシュの大きさのオーダーです(i7-7700KのL1データキャッシュは4x32kBです)。
もちろん、実際にデータに対して実行しなければならない計算が増えるほど、差異の重要性は低下します。 sum = std::accumulate(v.begin(), v.end(), sum);
をfor (int k : v) sum += std::sqrt(2.0*k);
に置き換えた場合の結果は次のとおりです。
他のプラットフォームでは結果が異なる場合があります。通常、パフォーマンスが重要な場合は、特定のユースケースのベンチマークを作成します。
私はまだ悪い習慣だと思いますが、私のチームがMSVC 2008とGCC 4.1を使用しているので、最新のコンパイラを使用していません。
以前、MSVC 2008でvtuneに表示されたホットスポットの多くは、文字列のコピーに起因していました。次のようなコードがありました。
String Something::id() const
{
return valid() ? m_id: "";
}
...独自のString型を使用していることに注意してください(プラグイン作成者が異なるコンパイラーを使用し、std :: string/std :: wstringの異なる非互換の実装を使用できるソフトウェア開発キットを提供しているため、これが必要でした)。
String :: String(const String&)がかなりの時間を要することを示すコールグラフサンプリングプロファイリングセッションに応じて、簡単な変更を加えました。上記の例のようなメソッドが最大の貢献者でした(実際、プロファイリングセッションでは、メモリ割り当てと割り当て解除が最大のホットスポットの1つであり、文字列コピーコンストラクターが割り当ての主要な貢献者でした)。
私が行った変更は簡単でした:
static String null_string;
const String& Something::id() const
{
return valid() ? m_id: null_string;
}
しかし、これは違いの世界を作りました!ホットスポットはその後のプロファイラーセッションで消滅し、これに加えて、アプリケーションのパフォーマンスを追跡するために多くの徹底的なユニットテストを行います。これらの簡単な変更の後、あらゆる種類のパフォーマンステスト時間が大幅に短縮されました。
結論:絶対的な最新のコンパイラを使用していませんが、(少なくともすべての場合において)確実に値を返すためにコピーを最適化するコンパイラに依存しているようには見えません。 MSVC 2010のような新しいコンパイラを使用している人には当てはまらないかもしれません。C++ 0xを使用して単純に右辺値参照を使用できるようになるのを楽しみにしています。値によるクラス。
[編集]ネイトが指摘したように、RVOは関数内で作成された一時的なものを返すことに適用されます。私の場合、そのような一時的なものはなく(空の文字列を作成する無効なブランチを除く)、RVOは適用されませんでした。
ちょっとちょっと:配列を関数から返すことは多くのプログラミング言語では一般的ではありません。それらのほとんどでは、配列への参照が返されます。 C++では、最も近いアナロジーはboost::shared_array
を返すことです
パフォーマンスが実際の問題である場合、移動セマンティクスはコピーよりも常に速くないことを認識してください。たとえば、小さな文字列の最適化を使用する文字列がある場合、小さな文字列の場合、移動コンストラクタは通常のコピーとまったく同じ量の作業を行う必要がありますコンストラクタ。