実際、C ++ 11以降、コピーのコストはstd::vector
ほとんどの場合なくなっています。
ただし、新しいベクトルを作成する(その後、ベクトルを破棄する)コストは依然として存在することに注意してください。ベクトルの容量を再利用したい場合は、値で返すのではなく、出力パラメーターを使用すると便利です。これは、C ++コアガイドラインのF.20に例外として記載されています。
比較してみましょう:
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を使用してみましょう。これは、16 GBのRAMがあり、この数値により8 GBが割り当てられないため(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)場合、時間は完全に同一です。実際、どちらの場合でも、メモリに8 GBの巨大なベクトルを1回だけ割り当てます。これは、BuildLargeVector1()を使用してもコピーが行われなかったことも証明します。コピーを実行するのに十分なRAMがありません!
の場合numIter = 2
、2番目のベクトルを再割り当てする代わりにベクトル容量を再利用すると、1.37倍速くなります。
の場合numIter = 256
、ベクトル容量の再利用(ベクトルの割り当て/割り当て解除を256回繰り返す代わりに...)は2.45倍高速です:)
time1はnumIter = 1
to からまでほぼ一定numIter = 256
であることがわかります。つまり、8 GBの1つの巨大なベクトルを割り当てると、32 MBの256ベクトルを割り当てる場合と同じくらいのコストがかかります。ただし、8 GBの1つの巨大なベクトルを割り当てると、32 MBの1つのベクトルを割り当てるよりも明らかにコストがかかるため、ベクトルの容量を再利用するとパフォーマンスが向上します。
numIter = 512
(MEM(V)= 16メガバイト)までnumIter = 8M
(MEM(V)= 1kBの)スイートスポットです:両方の方法は、早く正確で、より速くnumIterとvecSizeのすべての他の組み合わせよりも。これはおそらく、私のプロセッサのL3キャッシュサイズが8MBであるため、ベクトルがほぼ完全にキャッシュに収まるためです。time1
mem(v)= 8MB の突然のジャンプがなぜmem(v)= 16MBであるのかは、実際には説明しません。驚くことに、このスイートスポットでは、再利用しない方が実際にはわずかに高速です。私は本当にこれを説明しません。
numIter > 8M
物事が醜くなり始めるとき。どちらの方法も遅くなりますが、値によるベクトルの返却はさらに遅くなります。最悪の場合、単一のを1つだけ含むベクトルでint
は、値で返す代わりに容量を再利用すると、3.3倍速くなります。おそらく、これは、支配し始めるmalloc()の固定コストが原因です。
time2の曲線がtime1の曲線よりもどのように滑らかであるかに注意してください。ベクトル容量の再利用は一般的に高速であるだけでなく、おそらくより重要なことに、より予測可能です。
また、スイートスポットでは、64ビット整数の20億回の追加を約0.5秒で実行できたことに注意してください。これは、4.2Ghz 64ビットプロセッサで非常に最適です。8つのコアすべてを使用するために計算を並列化することで、より良い結果が得られます(上記のテストでは、一度に1つのコアのみを使用します。これは、CPU使用率を監視しながらテストを再実行して確認しました)。最高のパフォーマンスは、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);
。
結論
- 値で返す代わりに出力パラメーターを使用すると、容量を再利用することでパフォーマンスが向上する場合があります。
- 最近のデスクトップコンピューターでは、これは大きなベクター(> 16MB)と小さなベクター(<1kB)にのみ適用できるようです。
- 数百万/数十億の小さなベクトル(<1kB)を割り当てることは避けてください。可能であれば、キャパシティを再利用するか、さらに良い方法として、アーキテクチャを別の方法で設計します。
他のプラットフォームでは結果が異なる場合があります。通常どおり、パフォーマンスが重要な場合は、特定のユースケースのベンチマークを記述します。