web-dev-qa-db-ja.com

std :: vectorの最後にInsertまたはPush_back?

std::vectorの末尾に新しい要素を挿入するために、以下の2つの方法のパフォーマンスに違いはありますか?

方法1

std::vector<int> vec = { 1 };
vec.Push_back(2);
vec.Push_back(3);
vec.Push_back(4);
vec.Push_back(5);

方法2

std::vector<int> vec = { 1 };
int arr[] = { 2,3,4,5 };
vec.insert(std::end(vec), std::begin(arr), std::end(arr));

個人的に、私は方法2が好きです。それは、ニースで簡潔であり、配列からすべての新しい要素を一度に挿入するためです。だが

  • パフォーマンスに違いはありますか?
  • 結局、彼らは同じことをします。彼らはね?

更新

そもそも、すべての要素でベクトルを初期化しないのは、私のプログラムでは、条件に基づいて残りの要素を追加するためです。

16
jignatius

結局、彼らは同じことをします。彼らはね?

いいえ、違います。 std::vector::Push_back を使用する最初のメソッドは、 std::vector::insert と比較して、いくつかの再割り当てを受けます。

insertは、範囲をコピーする前に、現在の std::vector::capacity に従って内部的にメモリを割り当てます。詳細については、次の説明を参照してください。

std :: vector :: insertは定義により予約されていますか?


しかし、パフォーマンスに違いはありますか?

上記の理由により、2番目の方法ではパフォーマンスがわずかに向上します。たとえば、http://quick-bench.comを使用して、以下のクイックbenckマークを参照してください。

オンラインベンチマークを参照

enter image description here

または、パフォーマンスを測定するためのテストプログラムを記述します(コメントに記載されている@ 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 ++ 17AMD 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::insert2.7より約std::vector::Push_back倍高速であることを示しています。

他のコンパイラ(clang 8.0およびgcc 9.2)が実装に応じて何を言おうとしているのかを参照してください。 https://godbolt.org/z/DQrq51

16
JeJo

ベクトルを再割り当てする必要がある場合、2つのアプローチには違いがあるかもしれません。

2番目のメソッドは、insert()メンバー関数をイテレーターの範囲で1回呼び出します。

_vec.insert(std::end(vec), std::begin(arr), std::end(arr));
_

insert()ランダムアクセスイテレータを取得しているため、要素の挿入に必要なすべてのメモリを割り当てるための最適化を提供できます。つまり、範囲のサイズがわかっているため、要素をコピーする前にメモリ全体の割り当てを行うことができ、呼び出し中の再割り当ては行われません。

最初のメソッドであるPush_back()メンバー関数への個々の呼び出しは、挿入する要素の数と、最初にベクター用に予約されているメモリに応じて、いくつかの再割り当てをトリガーします。

上記で説明した最適化はforwardまたはbidirectional iteratorsでは使用できない場合があることに注意してください。挿入する要素の数を知るには範囲のサイズに線形時間がかかるためです。 。ただし、複数のメモリ割り当てに必要な時間は、これらのケースの範囲の長さを計算するのに必要な時間よりも短い可能性が高いため、おそらくこの最適化を実装しています。 input iteratorsの場合、シングルパスイテレータであるため、この最適化は不可能です。

12
眠りネロク

主な要因は、再割り当てです。 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};
_

enter image description here

再割り当てが行われることを確認するために、プッシュバックと挿入バージョンにvec.reserve(5)を追加した後、以下の結果が得られます。

enter image description here

5
Gaurav Sehgal

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を使用するのがより理想的な方法です。

4