web-dev-qa-db-ja.com

何が良いですか:ベクトル容量を予約するか、サイズに事前に割り当てるか、ループでプッシュバックしますか?

文字配列とセグメントサイズへのポインタを入力引数として受け取り、std::array<std::string>を必要とする別の関数を呼び出す関数があります。入力char配列が等しい部分に「セクション化」され、文字列配列が形成されるという考え方です。

入力char配列形式は、決定されたサイズのいくつかの小さな配列(または文字列)であり、連結されています。これらは、ゼロで終了しているとは見なされませんが、ゼロで終了しているとは見なされません。セグメントサイズ5と要素数10の例:

char k[] = "1234\0001234\0001234\0001234\0001234\0001234\0001234\0001234\0001234\0001234\000";
char m[] = "1234\00067890987654321\000234567809876\0005432\000\000\0003456789098";
char n[] = "12345678909876543211234567890987654321123456789098";

すべてのchar配列の長さは51(セグメント*要素+ 1)です。私の目標は、関数がリソースを効率的に使用できるようにすることです。最も重要なのは実行時間です。

猫の皮を剥ぐ方法はたくさんあるので、これに取り組む方法は2つ(または3つ)あります。問題は、どちらが「より良い」かということです。つまり、より速く、リソースの浪費が少ないということです。私は専門家ではないので、我慢してください。

ここでは、valuesが事前に割り当てられてから、各文字列に値が割り当てられます。

void myfnc_1(void *a_src, uint32_t a_segment) {
    // a_segment = 5 for example
    size_t nSize = GetSize(); // another method, gets 10
    std::vector<std::string> values(nSize);
    char* v = a_src; // take k, n or m for example

    for (size_t i = 0; i < nSize; ++i) {
        values.at(i).assign(v, a_segment);
        v += a_segment;
    }
}

ここでは、ベクトルは割り当てられていませんが、反復ごとに新しい文字列が追加されます。

void myfnc_1(void *a_src, uint32_t a_segment) {
    size_t nSize = GetSize();
    std::vector<std::string> values();
    char* v = a_src;

    for (size_t i = 0; i < nSize; ++i) {
        values.Push_back("");
        values.back().assign(v, a_segment);
        v += a_segment;
    }
}

3番目の方法があるかもしれません、それはより良いです。私はベクトルの経験があまりないので、正確にはわかりません。セグメントの長さと要素の数は、通常大きい(5、10)または小さい(100、10000)場合に違いがありますか?

最初の投稿、大ファン:)

9
daljaz

動的な再割り当てを回避するとパフォーマンスが向上するため、すべての要素を受け取るのに十分な大きさのベクトルメモリを用意してください。

NSizeがデフォルトのベクトル容量よりも大きい場合、2番目のソリューションではすべての要素を格納できるように再割り当てが必要になるため、最初のソリューションの方が効率的です。

Melkonがコメントしたように、reserveはさらに優れています。

void myfnc_1(void *a_src, uint32_t a_segment) {
    size_t nSize = GetSize();
    std::vector<std::string> values;
    values.reserve( nSize );
    char* v = a_src;

    for (size_t i = 0; i < nSize; ++i) {
        values.Push_back( std::string( v, a_segment ) );
        v += a_segment;
    }
}
4
jpo38

ベクトルへの要素の追加

ベクトルにデータを追加するには、いくつかの方法があります。

  • 空のベクトル、Push_back()要素をその中に作成します。
  • 空のベクトルを作成し、reserve()を使用して容量を割り当て、次にPush_back()要素をその中に割り当てます。
  • n要素のベクトルを作成し、インデックス付けとコピー代入を使用します。
  • 空のベクトル、emplace_back()要素をその中に作成します。
  • 空のベクトルを作成し、reserve()を使用して容量を割り当て、次にemplace_back()要素をその中に割り当てます。

他の方法があります、例えば。イテレータのペアでコンテナを作成するか、標準ライブラリアルゴリズムを介してコンテナを埋めます。ここではこれらについては考慮しません。

一般的な考慮事項

次の2つの考慮事項は、次の分析にとって重要です。

  • (再)割り当てを回避する:メモリ割り当てが遅い。再割り当てには、多くの場合、コンテナ内にすでにあるすべてのものを新しい場所にコピーすることが含まれます。
  • 不要な作業を避ける:デフォルトで作成するよりも、必要な要素を作成してから割り当てる方がよいでしょう。他の場所で要素を作成してからコピーするよりも、必要な場所で要素を作成することをお勧めします。

他の要因も選択したソリューションの効率に影響を与えますが、これらは私たちが直接制御できる重要な要因です。他の要因は、コードをプロファイリングすることで明らかになる可能性があります。

Push_back()

Push_back() copy-引数からPush_back()呼び出しへのベクトル内の要素を構築します。ベクトルsize() == capacity()の場合、再割り当てが実行されます。これにより、通常(常にではない場合があります)容量が2倍になり、既存の要素のすべてすべてが新しいストレージにコピーされる可能性があります。

事前割り当てのあるPush_back()

reserve()を使用すると、開始する前に要素に十分なメモリが割り当てられます。要素の数がわかっている(または合理的に推測できる)場合は、これを行う価値が常にあります。あなたが推測しているなら、過大評価は過小評価よりも優れています。

Push_back()呼び出しは、引き続き要素タイプのコピーコンストラクターを使用しますが、スペースが既に提供されているため、割り当てはありません。 reserve()呼び出し中に、単一の割り当てのコストを支払うだけです。既存の容量を超えて実行すると、Push_back()が再割り当てされ、容量が2倍になることがよくあります。これが、サイズの過大評価が優れている理由です。再割り当てが発生する可能性は低くなりますが、過小評価すると、再割り当てされる可能性が高くなるだけでなく、必要な量のほぼ2倍のメモリ割り当てが無駄になります。

「容量の倍増」動作は標準では指定されていませんが、サイズが不明なデータセットにPush_back()を使用するときに再割り当ての頻度を減らすように設計された一般的な実装であることに注意してください。

インデックス作成と要素の割り当て

ここでは、デフォルトで構築された要素の正しい数のベクトルを作成し、コピー代入演算子を使用して、それらを必要な要素に置き換えます。これには割り当てが1つしかないが、コピー割り当てが複雑なことを行うと遅くなる可能性がある。これは、サイズが不明な(または推測された)データセットでは実際には機能しません。要素のインデックス作成は、インデックスがsize()を超えないことがわかっている場合にのみ安全であり、さらに必要な場合はPush_back()またはサイズ変更に頼る必要があります。これは一般的な解決策としては適切ではありませんが、機能する場合があります。

emplace_back()

emplace_back()は、emplace_back()呼び出しの引数を使用して要素をインプレースで構築します。多くの場合、これは同等のPush_back()よりも高速です(常にではありません)。それでも、Push_back()と同じパターンで割り当て、容量を予約し、それを埋めてから、さらに必要なときに再割り当てします。同じ議論の多くが当てはまりますが、構築方法からいくつかの利益を得ることができます。

事前割り当てのあるemplace_back()

これは、C++ 11以降のコードベースの頼りになる戦略です。 emplace_back()の効率が得られ(可能な場合)、割り当ての繰り返しを回避できます。リストされているメカニズムの中で、これはほとんどの場合最速であると予想されます。

効率に関する注記

効率はいくつかの方法で測定できます。実行時間は一般的な指標ですが、常に最も適切であるとは限りません。どの戦略を使用するかについての一般的なアドバイスは、経験に基づいており、基本的にさまざまな効果を「平均化」して、最初に何をすべきかについての合理的なステートメントを提供します。いつものように、アプリケーションにとって何らかの効率が重要である場合、適切な場所を最適化していることを確認する唯一の方法は、コードをprofile変更してから、profile変更が望ましい効果をもたらしたことを示すためにもう一度。さまざまなデータ型、ハードウェア、I/O要件などがすべてこの種のタイミングに影響を与える可能性があり、特定のアプリケーションでこれらの影響がどのように組み合わされるかは、profileになるまでわかりません。

実例: http://coliru.stacked-crooked.com/a/83d23c2d0dcee2ff

この例では、上記の各アプローチを使用して、ベクトルに10,000個の文字列を入力します。それぞれの時間を計り、結果を印刷します。

これはあなたの質問に似ていますが、同一ではありません。結果は異なります!

emplace_back()reserve()が最速ですが、ここではインデックス作成と割り当ても高速であることに注意してください。これは、_std::string_に効率的なswap()があり、デフォルトのコンストラクターがあまり機能しないためと考えられます。他のアプローチは桁違いに遅くなります。

_#include <chrono>
#include <iostream>
#include <vector>

using Clock = std::chrono::high_resolution_clock;
using time_point = std::chrono::time_point<Clock>;

std::vector<std::string> strings = {"one", "two", "three", "four", "five"};

std::chrono::duration<double> vector_Push_back(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    for (size_t i = 0; i < n; ++i) {
        v.Push_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_Push_back_with_reserve(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    v.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        v.Push_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_element_assignment(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v(n);
    for (size_t i = 0; i < n; ++i) {
        v[i] = strings[i % strings.size()];
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_emplace_back(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    for (size_t i = 0; i < n; ++i) {
        v.emplace_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_emplace_back_with_reserve(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    v.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        v.emplace_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

int main() {
    const size_t n = 10000;
    std::cout << "vector Push_back: " << vector_Push_back(n).count() << "\n";
    std::cout << "vector Push_back with reserve: " << vector_Push_back(n).count() << "\n";
    std::cout << "vector element assignment: " << vector_element_assignment(n).count() << "\n";
    std::cout << "vector emplace_back: " << vector_emplace_back(n).count() << "\n";
    std::cout << "vector emplace_back with reserve: " << vector_emplace_back_with_reserve(n).count() << "\n";
}
_

結果:

_vector Push_back: 0.00205563
vector Push_back with reserve: 0.00152464
vector element assignment: 0.000610934
vector emplace_back: 0.00125141
vector emplace_back with reserve: 0.000545451
_

結論

ほとんどの新しいコードでは、reserve()emplace_back()(または古いコードの場合はPush_back())を使用すると、効率を最初に概算できます。それでも不十分な場合は、プロファイルを作成して、ボトルネックがどこにあるかを調べます。それはおそらくあなたが思っている場所ではないでしょう。

16
Andrew

デフォルトのコンストラクターを呼び出すために括弧を使用しないでください。

_Push_back_は、容量を超えるたびに追加の再割り当てが必要です。したがって、オプション2は、再割り当てを回避するために十分なスペースを予約することで改善できます。また、空の文字列をプッシュして後で再割り当てするよりも、文字列を直接プッシュする方が効率的です。そして、あなたのニーズに非常に便利な_std::string_のコンストラクターがあります: from sequence(5)string (const char* s, size_t n);

オプション1について:ベクトル全体を事前に割り当てるには、初期化のために各要素を1回作成し、割り当てのためにさらに別の時間を作成する必要があります。要素を作成せずに、本当に必要な要素を直接_Push_back_予約することをお勧めします。

これはそれらの改善を使用したコードです:

_void myfnc_1(void *a_src, uint32_t a_segment)
{
    std::vector<std::string> values;
    size_t nSize = GetSize( );
    values.reserve(nSize);
    char* v = static_cast<char*> ( a_src );

    for (size_t i = 0; i < nSize; ++i)
    {
        values.Push_back( std::string( v, a_segment) );
        v += a_segment;
    }
}
_