std::vector
の末尾に新しい要素を挿入するために、以下の2つの方法のパフォーマンスに違いはありますか?
std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);
std::vector<int> vec = { 1 };
int arr[] = { 2,3,4,5 };
vec.insert(std::end(vec), std::begin(arr), std::end(arr));
個人的に、私は方法2が好きです。それは、ニースで簡潔であり、配列からすべての新しい要素を一度に挿入するためです。だが
そもそも、すべての要素でベクトルを初期化しないのは、私のプログラムでは、条件に基づいて残りの要素を追加するためです。
結局、彼らは同じことをします。彼らはね?
いいえ、違います。 std::vector::Push_back
を使用する最初のメソッドは、 std::vector::insert
と比較して、いくつかの再割り当てを受けます。
insert
は、範囲をコピーする前に、現在の std::vector::capacity
に従って内部的にメモリを割り当てます。詳細については、次の説明を参照してください。
std :: vector :: insertは定義により予約されていますか?
しかし、パフォーマンスに違いはありますか?
上記の理由により、2番目の方法ではパフォーマンスがわずかに向上します。たとえば、http://quick-bench.comを使用して、以下のクイックbenckマークを参照してください。
または、パフォーマンスを測定するためのテストプログラムを記述します(コメントに記載されている@ Some Programmer dudeとして)。以下はサンプルテストプログラムです。
#include <iostream>
#include <chrono>
#include <algorithm>
#include <vector>
using namespace std::chrono;
class Timer final
{
private:
time_point<high_resolution_clock> _startTime;
public:
Timer() noexcept
: _startTime{ high_resolution_clock::now() }
{}
~Timer() noexcept { Stop(); }
void Stop() noexcept
{
const auto endTime = high_resolution_clock::now();
const auto start = time_point_cast<microseconds>(_startTime).time_since_Epoch();
const auto end = time_point_cast<microseconds>(endTime).time_since_Epoch();
const auto durationTaken = end - start;
const auto duration_ms = durationTaken * 0.001;
std::cout << durationTaken.count() << "us (" << duration_ms.count() << "ms)\n";
}
};
// Method 1: Push_back
void Push_back()
{
std::cout << "Push_backing: ";
Timer time{};
for (auto i{ 0ULL }; i < 1000'000; ++i)
{
std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);
}
}
// Method 2: insert_range
void insert_range()
{
std::cout << "range-inserting: ";
Timer time{};
for (auto i{ 0ULL }; i < 1000'000; ++i)
{
std::vector<int> vec = { 1 };
int arr[] = { 2,3,4,5 };
vec.insert(std::end(vec), std::cbegin(arr), std::cend(arr));
}
}
int main()
{
Push_back();
insert_range();
return 0;
}
私のシステムでビルドをリリース(MSVS2019:/ Ox/std:c ++ 17、AMD Ryzen 7 2700x(8コア、3.70 Ghz)、x64 Windows 10)
// Build - 1
Push_backing: 285199us (285.199ms)
range-inserting: 103388us (103.388ms)
// Build - 2
Push_backing: 280378us (280.378ms)
range-inserting: 104032us (104.032ms)
// Build - 3
Push_backing: 281818us (281.818ms)
range-inserting: 102803us (102.803ms)
これは、指定されたシナリオでstd::vector::insert
が2.7
より約std::vector::Push_back
倍高速であることを示しています。
他のコンパイラ(clang 8.0およびgcc 9.2)が実装に応じて何を言おうとしているのかを参照してください。 https://godbolt.org/z/DQrq51
ベクトルを再割り当てする必要がある場合、2つのアプローチには違いがあるかもしれません。
2番目のメソッドは、insert()
メンバー関数をイテレーターの範囲で1回呼び出します。
_vec.insert(std::end(vec), std::begin(arr), std::end(arr));
_
insert()
はランダムアクセスイテレータを取得しているため、要素の挿入に必要なすべてのメモリを割り当てるための最適化を提供できます。つまり、範囲のサイズがわかっているため、要素をコピーする前にメモリ全体の割り当てを行うことができ、呼び出し中の再割り当ては行われません。
最初のメソッドであるPush_back()
メンバー関数への個々の呼び出しは、挿入する要素の数と、最初にベクター用に予約されているメモリに応じて、いくつかの再割り当てをトリガーします。
上記で説明した最適化はforwardまたはbidirectional iteratorsでは使用できない場合があることに注意してください。挿入する要素の数を知るには範囲のサイズに線形時間がかかるためです。 。ただし、複数のメモリ割り当てに必要な時間は、これらのケースの範囲の長さを計算するのに必要な時間よりも短い可能性が高いため、おそらくこの最適化を実装しています。 input iteratorsの場合、シングルパスイテレータであるため、この最適化は不可能です。
主な要因は、再割り当てです。 vector
は新しい要素のためのスペースを作る必要があります。
これらの3つのシンペットを考えてみましょう。
_ //pushback
std::vector<int> vec = {1};
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);
//insert
std::vector<int> vec = {1};
int arr[] = {2,3,4,5};
vec.insert(std::end(vec), std::begin(arr), std::end(arr));
//cosntruct
std::vector<int> vec = {1,2,3,4,5};
_
再割り当てが行われることを確認するために、プッシュバックと挿入バージョンにvec.reserve(5)
を追加した後、以下の結果が得られます。
Push_back
は単一の要素を挿入するため、最悪の場合、複数の再割り当てが発生する可能性があります。
例として、初期容量が2で、再割り当てごとに2倍に増加する場合を考えます。その後
std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3); // need to reallocate, capacity is 4
vec.Push_back(4);
vec.Push_back(5); // need to reallocate, capacity is 8
もちろん、呼び出すことにより、不要な再割り当てを防ぐことができます
vec.reserve(num_elements_to_Push);
ただし、配列から挿入する場合は、insert
を使用するのがより理想的な方法です。