web-dev-qa-db-ja.com

std :: vectorは普通の配列よりもずっと遅いですか?

std::vectorが「配列として実装されている」というのが一般的な知恵だといつも思っていました。今日、私はダウンしてテストしましたが、そうではないようです:

テスト結果は次のとおりです。

UseArray completed in 2.619 seconds
UseVector completed in 9.284 seconds
UseVectorPushBack completed in 14.669 seconds
The whole thing completed in 26.591 seconds

それは約3-4倍遅いです! 「vectorは数ナノ秒遅くなる可能性がある」というコメントを正当化するものではありません。

そして、私が使用したコード:

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
    public:
        TestTimer(const std::string & name) : name(name),
            start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
        {
        }

        ~TestTimer()
        {
            using namespace std;
            using namespace boost;

            posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
            posix_time::time_duration d = now - start;

            cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
                " seconds" << endl;
        }

    private:
        std::string name;
        boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }

    unsigned char r, g, b;
};

void UseVector()
{
    TestTimer t("UseVector");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPushBack");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.Push_back(Pixel(255, 0, 0));
    }
}

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        free(pixels);
    }
}

int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorPushBack();

    return 0;
}

私はそれを間違っているのですか?それとも、このパフォーマンスの神話を破壊しただけなのでしょうか?

Visual Studio 2005 でリリースモードを使用しています。


Visual C++ では、#define _SECURE_SCL 0UseVectorを半分に減らします(4秒に短縮します)。これは本当に巨大です、IMO。

201
kizzx2

次を使用して:

g ++ -O3 Time.cpp -I <MyBoost>
./ a.out
UseArrayは2.196秒で完了しました
UseVectorは4.412秒で完了しました
UseVectorPushBackは8.017秒で完了しました
すべてが14.626秒で完了しました

したがって、配列はベクトルの2倍の速さです。

ただしコードを詳細に調べた後、これが予想されます。ベクトルを2回、配列を1回だけ実行します。注:resize()ベクトルを使用すると、メモリを割り当てるだけでなく、ベクトルを実行して各メンバーでコンストラクターを呼び出します。

ベクトルが各オブジェクトを1回だけ初期化するようにコードをわずかに再配置します。

 std::vector<Pixel>  pixels(dimensions * dimensions, Pixel(255,0,0));

今再び同じタイミングを行います:

g ++ -O3 Time.cpp -I <MyBoost>
./ a.out
UseVectorは2.216秒で完了しました

ベクターのパフォーマンスは、配列よりもわずかに劣ります。 IMOのこの違いは取るに足りないものであり、テストに関連していないものが原因である可能性があります。

また、コンストラクター/デストラクターも呼び出されないため、UseArrray()メソッドでPixelオブジェクトを正しく初期化/破棄していないことも考慮します(この単純なクラスでは問題ではないかもしれませんが、少し複雑です(すなわち、ポインターまたはポインターを持つメンバー)が問題を引き起こします。

251
Martin York

いい質問ですね。ベクトルテストを高速化する簡単な修正を見つけることを期待してここに来ました。それは私が期待したようにはうまくいきませんでした!

最適化は役立ちますが、それだけでは十分ではありません。最適化を有効にしても、UseArrayとUseVectorの2倍のパフォーマンスの違いが見られます。興味深いことに、UseVectorは、最適化なしのUseVectorPushBackよりも大幅に低速でした。

# g++ -Wall -Wextra -pedantic -o vector vector.cpp
# ./vector
UseArray completed in 20.68 seconds
UseVector completed in 120.509 seconds
UseVectorPushBack completed in 37.654 seconds
The whole thing completed in 178.845 seconds
# g++ -Wall -Wextra -pedantic -O3 -o vector vector.cpp
# ./vector
UseArray completed in 3.09 seconds
UseVector completed in 6.09 seconds
UseVectorPushBack completed in 9.847 seconds
The whole thing completed in 19.028 seconds

アイデア#1-mallocの代わりにnew []を使用する

UseArrayでmalloc()new[]に変更して、オブジェクトが構築されるようにしました。また、個々のフィールドの割り当てからPixelインスタンスの割り当てに変更します。ああ、内部ループ変数の名前をjに変更します。

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {   
        int dimension = 999;

        // Same speed as malloc().
        Pixel * pixels = new Pixel[dimension * dimension];

        for(int j = 0 ; j < dimension * dimension; ++j)
            pixels[j] = Pixel(255, 0, 0);

        delete[] pixels;
    }
}

驚いたことに(私にとって)、これらの変更はどれも違いをもたらしませんでした。すべてのピクセルをデフォルトで構成するnew[]への変更もありません。 gccはnew[]を使用するときはデフォルトのコンストラクター呼び出しを最適化できますが、vectorを使用するときは最適化できないようです。

アイデア#2-繰り返されるoperator []呼び出しを削除する

また、トリプルoperator[]ルックアップを削除し、pixels[j]への参照をキャッシュしようとしました。それは実際にUseVectorを遅くしました!おっと。

for(int j = 0; j < dimension * dimension; ++j)
{
    // Slower than accessing pixels[j] three times.
    Pixel &pixel = pixels[j];
    pixel.r = 255;
    pixel.g = 0;
    pixel.b = 0;
}

# ./vector 
UseArray completed in 3.226 seconds
UseVector completed in 7.54 seconds
UseVectorPushBack completed in 9.859 seconds
The whole thing completed in 20.626 seconds

アイデア#3-コンストラクターを削除する

コンストラクタを完全に削除するのはどうですか?その後、おそらくgccは、ベクトルの作成時にすべてのオブジェクトの構築を最適化できます。 Pixelを次のように変更するとどうなりますか?

struct Pixel
{
    unsigned char r, g, b;
};

結果:約10%高速化。配列よりもさらに遅い。ふむ.

# ./vector 
UseArray completed in 3.239 seconds
UseVector completed in 5.567 seconds

アイデア#4-ループインデックスの代わりにイテレータを使用する

ループインデックスの代わりにvector<Pixel>::iteratorを使用してはどうですか?

for (std::vector<Pixel>::iterator j = pixels.begin(); j != pixels.end(); ++j)
{
    j->r = 255;
    j->g = 0;
    j->b = 0;
}

結果:

# ./vector 
UseArray completed in 3.264 seconds
UseVector completed in 5.443 seconds

いいえ、違いはありません。少なくとも遅くはありません。これは、Pixel&参照を使用した#2と同様のパフォーマンスになると思いました。

結論

いくつかのスマートCookieが配列ループと同じくらい高速にベクトルループを作成する方法を見つけたとしても、これはstd::vectorのデフォルトの動作をうまく語りません。コンパイラがC++のすべてを最適化し、STLコンテナを生の配列と同じくらい高速にするのに十分なほどスマートであるために。

一番下の行は、コンパイラがstd::vectorを使用するときに、no-opのデフォルトコンストラクター呼び出しを最適化することができないということです。プレーンnew[]を使用すると、それらを最適化して最適化します。ただし、std::vectorではありません。コードを書き直して、この周辺のマントラに直面するコンストラクター呼び出しを排除できたとしても、「コンパイラーはあなたよりも賢い。STLはプレーンCと同じくらい高速です。心配する必要はありません。」

52
John Kugelman

これは古くて人気のある質問です。

この時点で、多くのプログラマーがC++ 11で作業することになります。また、C++ 11では、書かれたOPのコードは、UseArrayまたはUseVectorに対して同等に高速に実行されます。

UseVector completed in 3.74482 seconds
UseArray completed in 3.70414 seconds

基本的な問題は、Pixel構造が初期化されていない間に、std::vector<T>::resize( size_t, T const&=T() )がデフォルトで構築されたPixelを取り、がそれをコピーすることです。コンパイラは、初期化されていないデータをコピーするように求められていることに気づかなかったため、実際にコピーを実行しました。

C++ 11では、std::vector<T>::resizeには2つのオーバーロードがあります。最初はstd::vector<T>::resize(size_t)、もう1つはstd::vector<T>::resize(size_t, T const&)です。つまり、2番目の引数を指定せずにresizeを呼び出すと、単にデフォルトの構成が作成され、コンパイラはデフォルトの構成が何も実行しないことを認識するのに十分なため、バッファーのパスをスキップします。

(移動可能、構築可能、およびコピー不可のタイプを処理するために追加された2つのオーバーロード-初期化されていないデータを操作するときのパフォーマンスの向上はボーナスです)。

Push_backソリューションはfencepostチェックも行うため、速度が低下するため、mallocバージョンよりも低速のままです。

実例 (タイマーもchrono::high_resolution_clockに置き換えました)。

通常は初期化が必要な構造を持っているが、バッファーを増やした後に処理したい場合は、カスタムstd::vectorアロケーターを使用してこれを行うことができます。その後、より通常のstd::vectorに移動する場合は、allocator_traitsを慎重に使用し、==をオーバーライドするとそれがうまくいくと思いますが、確信はありません。

公平を期すために、mallocバージョンと呼ぶように、C++実装とC実装を比較することはできません。 mallocはオブジェクトを作成しません-rawメモリのみを割り当てます。コンストラクターを呼び出さずにそのメモリをオブジェクトとして扱うのは貧弱なC++です(おそらく無効です-言語弁護士に任せます)。

ただし、mallocをnew Pixel[dimensions*dimensions]に変更し、delete [] pixelsに自由に変更しても、Pixelの単純な実装と大きな違いはありません。私のボックス(E6600、64ビット)の結果は次のとおりです。

UseArray completed in 0.269 seconds
UseVector completed in 1.665 seconds
UseVectorPushBack completed in 7.309 seconds
The whole thing completed in 9.244 seconds

しかし、わずかな変更により、表は次のように変わります。

Pixel.h

struct Pixel
{
    Pixel();
    Pixel(unsigned char r, unsigned char g, unsigned char b);

    unsigned char r, g, b;
};

Pixel.cc

#include "Pixel.h"

Pixel::Pixel() {}
Pixel::Pixel(unsigned char r, unsigned char g, unsigned char b) 
  : r(r), g(g), b(b) {}

main.cc

#include "Pixel.h"
[rest of test harness without class Pixel]
[UseArray now uses new/delete not malloc/free]

このようにコンパイルされました:

$ g++ -O3 -c -o Pixel.o Pixel.cc
$ g++ -O3 -c -o main.o main.cc
$ g++ -o main main.o Pixel.o

非常に異なる結果が得られます。

UseArray completed in 2.78 seconds
UseVector completed in 1.651 seconds
UseVectorPushBack completed in 7.826 seconds
The whole thing completed in 12.258 seconds

Pixelの非インラインコンストラクターを使用すると、std :: vectorは生の配列に勝ちます。

Std :: vectorとstd:allocatorによる割り当ての複雑さは、単純なnew Pixel[n]と同じくらい効果的に最適化するには多すぎるように見えます。ただし、ループの外に移動してベクトル/配列を作成するテスト関数をいくつか調整することにより、ベクトルアクセスではなく割り当てに問題があることがわかります。

void UseVector()
{
    TestTimer t("UseVector");

    int dimension = 999;
    std::vector<Pixel> pixels;
    pixels.resize(dimension * dimension);

    for(int i = 0; i < 1000; ++i)
    {
        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

そして

void UseArray()
{
    TestTimer t("UseArray");

    int dimension = 999;
    Pixel * pixels = new Pixel[dimension * dimension];

    for(int i = 0; i < 1000; ++i)
    {
        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
    delete [] pixels;
}

次の結果が得られました。

UseArray completed in 0.254 seconds
UseVector completed in 0.249 seconds
UseVectorPushBack completed in 7.298 seconds
The whole thing completed in 7.802 seconds

これから学ぶことができるのは、std :: vectorはアクセスのために生の配列に匹敵することですが、ベクトル/配列を何度も作成および削除する必要がある場合、単純な配列を作成するよりも複雑なオブジェクトを作成する方が時間がかかります要素のコンストラクターがインライン化されていない場合。これは非常に驚くべきことではないと思います。

34
camh

これで試してください:

void UseVectorCtor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));
    }
}

アレイとほぼ同じパフォーマンスが得られます。

vectorについてのことは、配列よりもはるかに一般的なツールであることです。そして、それはhowを使用することを考慮する必要があることを意味します。さまざまな方法で使用でき、アレイにはない機能を提供します。また、目的のために「間違った」使い方をすると、多くのオーバーヘッドが発生しますが、正しく使うと、通常は基本的にオーバーヘッドのないデータ構造になります。この場合、問題はベクトルを個別に初期化し(すべての要素にデフォルトのctorを呼び出すようにさせる)、各要素を正しい値で個別に上書きすることです。これは、配列を使用して同じことを行う場合よりも、コンパイラーが離れて最適化することははるかに困難です。これが、ベクターがまさにそれを可能にするコンストラクターを提供する理由です:N要素を値Xで初期化します。

そして、それを使用すると、ベクトルは配列と同じくらい高速です。

だから、あなたはパフォーマンスの神話を破壊していません。しかし、ベクトルを最適に使用する場合にのみ真実であることが示されました。これも非常に良い点です。 :)

明るい面では、実際には最速であることが判明したのは最も単純なの使用法です。私のコードスニペット(単一行)と、パフォーマンスの違いを完全に排除しないヒープおよび調整と最適化のヒープを含むジョンクーゲルマンの答えとを比較すると、vectorがかなり巧妙に設計されているすべて。アレイと同等の速度を得るためにフープをジャンプする必要はありません。それどころか、できるだけ簡単なソリューションを使用する必要があります。

26
jalf

私が最初にあなたのコードを見たとき、それはほとんど公平な比較ではありませんでした。リンゴとリンゴを比較しているのではないのは間違いない。だから、すべてのテストでコンストラクターとデストラクタが呼び出されると考えました。次に比較します。

const size_t dimension = 1000;

void UseArray() {
    TestTimer t("UseArray");
    for(size_t j = 0; j < dimension; ++j) {
        Pixel* pixels = new Pixel[dimension * dimension];
        for(size_t i = 0 ; i < dimension * dimension; ++i) {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = (unsigned char) (i % 255);
        }
        delete[] pixels;
    }
}

void UseVector() {
    TestTimer t("UseVector");
    for(size_t j = 0; j < dimension; ++j) {
        std::vector<Pixel> pixels(dimension * dimension);
        for(size_t i = 0; i < dimension * dimension; ++i) {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = (unsigned char) (i % 255);
        }
    }
}

int main() {
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();

    return 0;
}

私の考えでは、このセットアップでは、正確に同じである必要があります。判明した、私は間違っていた。

UseArray completed in 3.06 seconds
UseVector completed in 4.087 seconds
The whole thing completed in 10.14 seconds

では、なぜこの30%のパフォーマンスの損失が発生したのでしょうか? STLのヘッダーにはすべてが含まれているため、コンパイラーは必要なすべてを理解できるはずでした。

私の考えは、ループがすべての値をデフォルトのコンストラクターに初期化する方法にあるということでした。そこで、テストを実行しました。

class Tester {
public:
    static int count;
    static int count2;
    Tester() { count++; }
    Tester(const Tester&) { count2++; }
};
int Tester::count = 0;
int Tester::count2 = 0;

int main() {
    std::vector<Tester> myvec(300);
    printf("Default Constructed: %i\nCopy Constructed: %i\n", Tester::count, Tester::count2);

    return 0;
}

結果は私が疑ったとおりでした:

Default Constructed: 1
Copy Constructed: 300

これは明らかにスローダウンの原因であり、ベクターがデフォルトの構築オブジェクトから要素を初期化するためにコピーコンストラクターを使用するという事実です。

これは、ベクターの構築中に次の擬似操作順序が発生することを意味します。

Pixel pixel;
for (auto i = 0; i < N; ++i) vector[i] = pixel;

これは、コンパイラーによって作成された暗黙のコピーコンストラクターにより、次のように展開されます。

Pixel pixel;
for (auto i = 0; i < N; ++i) {
    vector[i].r = pixel.r;
    vector[i].g = pixel.g;
    vector[i].b = pixel.b;
}

したがって、デフォルトのPixelは初期化されないままで、残りの初期化されるはデフォルトのPixelの-​​初期化されない値です。

New[]/Delete[]を使用した別の状況と比較して:

int main() {
    Tester* myvec = new Tester[300];

    printf("Default Constructed: %i\nCopy Constructed:%i\n", Tester::count, Tester::count2);

    delete[] myvec;

    return 0;
}

Default Constructed: 300
Copy Constructed: 0

これらはすべて初期化されていない値のままであり、シーケンスの二重反復はありません。

この情報を武器に、どのようにテストできますか?暗黙のコピーコンストラクタを上書きしてみましょう。

Pixel(const Pixel&) {}

そして結果は?

UseArray completed in 2.617 seconds
UseVector completed in 2.682 seconds
The whole thing completed in 5.301 seconds

要約すると、非常に頻繁に数百のベクトルを作成している場合:アルゴリズムを再考

いずれにせよ、STL実装は何らかの理由で遅くなることはなく、まさにあなたが求めることをするだけです。もっとよく知ってほしい。

21

checked iterators を無効にして、リリースモードでビルドしてみてください。パフォーマンスの違いはあまり見られないはずです。

7
kloffy

GNUのSTL(およびその他)は、vector<T>(n)を指定すると、デフォルトでプロトタイプオブジェクトT()を構築します-コンパイラーは空のコンストラクターを最適化します-しかし、メモリアドレスにあったガベージのコピーオブジェクトのSTLの__uninitialized_fill_n_auxによって取得されます。これは、そのオブジェクトのコピーをベクターのデフォルト値としてループします。そのため、「my」STLはループ構築ではなく、ループ/コピーを構築しています。それは直感的ではありませんが、この点に関する最近のstackoverflowの質問にコメントしたときに覚えていたはずです:参照カウントされたオブジェクトなどのために構成/コピーはより効率的です。

そう:

vector<T> x(n);

または

vector<T> x;
x.resize(n);

is-多くのSTL実装で-次のようなもの:

T temp;
for (int i = 0; i < n; ++i)
    x[i] = temp;

問題は、コンパイラオプティマイザーの現在の世代がtempが初期化されていないガベージであり、ループとデフォルトのコピーコンストラクターの呼び出しを最適化できないという洞察から機能しないように見えることです。コンパイラーは絶対にこれを最適化すべきではない、と信じることができます。上記を書いているプログラマーは、ゴミの後でも、すべてのオブジェクトがループ後に同一になるという合理的な期待を持っているからです(「同一」/演算子== vs memcmp/operator =などが適用されます)。コンパイラーは、std :: vector <>のより大きなコンテキスト、またはこの最適化が安全であることを示唆するデータのその後の使用法について、さらなる洞察を得ることは期待できません。

これは、より明白で直接的な実装とは対照的です。

for (int i = 0; i < n; ++i)
    x[i] = T();

コンパイラが最適化することを期待できます。

ベクトルの動作のこの側面の正当性についてもう少し明確にするために、以下を考慮してください。

std::vector<big_reference_counted_object> x(10000);

10000個の独立したオブジェクトと同じデータを参照する10000個のオブジェクトを作成する場合、明らかに大きな違いがあります。偶然のC++ユーザーが誤って非常に高価なことをすることを防ぐ利点は、最適化が困難なコピー構築の非常に小さな現実のコストを上回るという合理的な議論があります。

元の回答(参照用/コメントの意味を理解するため):チャンスはありません。少なくともスペースを賢明に確保する場合、ベクトルは配列と同じくらい高速です。 ...

4
Tony Delroy

マーティンヨークの答え 気になるのは、カーペットの下で初期化の問題を解決しようとする試みのように思えるからです。しかし、彼は、パフォーマンスの問題の原因として冗長なデフォルト構造を特定するのが正しい。

[編集:Martinの答えは、デフォルトのコンストラクタを変更することを提案しなくなりました。]

当面の問題については、代わりにvector<Pixel> ctorの2パラメーターバージョンを呼び出すことができます。

std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));

これは、一定の値で初期化する場合に機能しますが、これは一般的なケースです。しかし、より一般的な問題は次のとおりです:定数値よりも複雑なもので効率的に初期化するにはどうすればよいですか?

これには、イテレータアダプタであるback_insert_iteratorを使用できます。一般的な考え方はintsに対しても同様に機能しますが、Pixelsのベクトルの例を次に示します。

#include <iterator>
// Simple functor return a list of squares: 1, 4, 9, 16...
struct squares {
    squares() { i = 0; }
    int operator()() const { ++i; return i * i; }

private:
    int i;
};

...

std::vector<int> v;
v.reserve(someSize);     // To make insertions efficient
std::generate_n(std::back_inserter(v), someSize, squares());

または、copy()の代わりにtransform()またはgenerate_n()を使用できます。

欠点は、初期値を構築するためのロジックを別のクラスに移動する必要があることです。これは、インプレースにするよりも便利ではありません(ただし、C++ 1xのラムダを使用するとさらに便利になります)。また、これはmalloc()ベースの非STLバージョンほど高速ではないことを期待していますが、各要素に対して1つの構築のみを行うため、近いと予想しています。

3
j_random_hacker

ベクトルのものはさらにPixelコンストラクターを呼び出しています。

それぞれが、あなたが計時しているほぼ100万の実行を引き起こしています。

編集:それから外側の1 ... 1000ループがあるので、10億のctorを呼び出します!

編集2:UseArrayの場合の逆アセンブリを見るのは面白いでしょう。オプティマイザは、CPUを焼く以外の効果がないため、全体を最適化できます。

2
Graham Perks

私のラップトップはLenova G770(4 GBのRAM)です。

OSはWindows 7 64ビット(ラップトップを搭載したもの)

コンパイラは MinGW 4.6.1。

IDEは Code :: Blocks です。

最初の投稿のソースコードをテストします。

結果

O2最適化

UseArrayは2.841秒で完了しました

UseVectorは2.548秒で完了しました

UseVectorPushBackは11.95秒で完了しました

すべてが17.342秒で完了しました

システムの一時停止

O3最適化

UseArrayは1.452秒で完了しました

UseVectorは2.514秒で完了しました

UseVectorPushBackは12.967秒で完了しました

すべてが16.937秒で完了しました

O3最適化では、ベクターのパフォーマンスが低下するようです。

ループを

    pixels[i].r = i;
    pixels[i].g = i;
    pixels[i].b = i;

O2とO3の下での配列とベクトルの速度はほぼ同じです。

1
StereoMatching

一部のプロファイラーデータ(ピクセルは32ビットに調整されます):

g++ -msse3 -O3 -ftree-vectorize -g test.cpp -DNDEBUG && ./a.out
UseVector completed in 3.123 seconds
UseArray completed in 1.847 seconds
UseVectorPushBack completed in 9.186 seconds
The whole thing completed in 14.159 seconds

ブラ

[email protected]:~$ opannotate --source libcchem/src/a.out  | grep "Total samples for file" -A3
Overflow stats not available
 * Total samples for file : "/usr/include/c++/4.4/ext/new_allocator.h"
 *
 * 141008 52.5367
 */
--
 * Total samples for file : "/home/andrey/libcchem/src/test.cpp"
 *
 *  61556 22.9345
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_vector.h"
 *
 *  41956 15.6320
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_uninitialized.h"
 *
 *  20956  7.8078
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_construct.h"
 *
 *   2923  1.0891
 */

allocatorで:

               :      // _GLIBCXX_RESOLVE_LIB_DEFECTS
               :      // 402. wrong new expression in [some_] allocator::construct
               :      void
               :      construct(pointer __p, const _Tp& __val)
141008 52.5367 :      { ::new((void *)__p) _Tp(__val); }

vector

               :void UseVector()
               :{ /* UseVector() total:  60121 22.3999 */
...
               :
               :
 10790  4.0201 :        for (int i = 0; i < dimension * dimension; ++i) {
               :
   495  0.1844 :            pixels[i].r = 255;
               :
 12618  4.7012 :            pixels[i].g = 0;
               :
  2253  0.8394 :            pixels[i].b = 0;
               :
               :        }

アレイ

               :void UseArray()
               :{ /* UseArray() total:  35191 13.1114 */
               :
...
               :
   136  0.0507 :        for (int i = 0; i < dimension * dimension; ++i) {
               :
  9897  3.6874 :            pixels[i].r = 255;
               :
  3511  1.3081 :            pixels[i].g = 0;
               :
 21647  8.0652 :            pixels[i].b = 0;

オーバーヘッドのほとんどは、コピーコンストラクターにあります。例えば、

    std::vector < Pixel > pixels;//(dimension * dimension, Pixel());

    pixels.reserve(dimension * dimension);

    for (int i = 0; i < dimension * dimension; ++i) {

        pixels[i].r = 255;

        pixels[i].g = 0;

        pixels[i].b = 0;
    }

アレイと同じパフォーマンスです。

1
Anycorn

しばらくの間、私は望んでいたいくつかの広範なテストを行いました。これを共有することもできます。

これは、Windows 8.1およびUbuntu 16.04上のデュアルブートマシンi7-3770、16 GB Ram、x86_64です。詳細および結論、以下の備考。 MSVS 2017とg ++の両方をテストしました(WindowsとLinuxの両方で)。

テストプログラム

#include <iostream>
#include <chrono>
//#include <algorithm>
#include <array>
#include <locale>
#include <vector>
#include <queue>
#include <deque>

// Note: total size of array must not exceed 0x7fffffff B = 2,147,483,647B
//  which means that largest int array size is 536,870,911
// Also image size cannot be larger than 80,000,000B
constexpr int long g_size = 100000;
int g_A[g_size];


int main()
{
    std::locale loc("");
    std::cout.imbue(loc);
    constexpr int long size = 100000;  // largest array stack size

    // stack allocated c array
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    int A[size];
    for (int i = 0; i < size; i++)
        A[i] = i;

    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "c-style stack array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "c-style stack array size=" << sizeof(A) << "B\n\n";

    // global stack c array
    start = std::chrono::steady_clock::now();
    for (int i = 0; i < g_size; i++)
        g_A[i] = i;

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "global c-style stack array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "global c-style stack array size=" << sizeof(g_A) << "B\n\n";

    // raw c array heap array
    start = std::chrono::steady_clock::now();
    int* AA = new int[size];    // bad_alloc() if it goes higher than 1,000,000,000
    for (int i = 0; i < size; i++)
        AA[i] = i;

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "c-style heap array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "c-style heap array size=" << sizeof(AA) << "B\n\n";
    delete[] AA;

    // std::array<>
    start = std::chrono::steady_clock::now();
    std::array<int, size> AAA;
    for (int i = 0; i < size; i++)
        AAA[i] = i;
    //std::sort(AAA.begin(), AAA.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::array size=" << sizeof(AAA) << "B\n\n";

    // std::vector<>
    start = std::chrono::steady_clock::now();
    std::vector<int> v;
    for (int i = 0; i < size; i++)
        v.Push_back(i);
    //std::sort(v.begin(), v.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::vector duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::vector size=" << v.size() * sizeof(v.back()) << "B\n\n";

    // std::deque<>
    start = std::chrono::steady_clock::now();
    std::deque<int> dq;
    for (int i = 0; i < size; i++)
        dq.Push_back(i);
    //std::sort(dq.begin(), dq.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::deque duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::deque size=" << dq.size() * sizeof(dq.back()) << "B\n\n";

    // std::queue<>
    start = std::chrono::steady_clock::now();
    std::queue<int> q;
    for (int i = 0; i < size; i++)
        q.Push(i);

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::queue duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::queue size=" << q.size() * sizeof(q.front()) << "B\n\n";
}

結果

//////////////////////////////////////////////////////////////////////////////////////////
// with MSVS 2017:
// >> cl /std:c++14 /Wall -O2 array_bench.cpp
//
// c-style stack array duration=0.15ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.130ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.90ms
// c-style heap array size=4B
//
// std::array duration=0.20ms
// std::array size=400,000B
//
// std::vector duration=0.544ms
// std::vector size=400,000B
//
// std::deque duration=1.375ms
// std::deque size=400,000B
//
// std::queue duration=1.491ms
// std::queue size=400,000B
//
//////////////////////////////////////////////////////////////////////////////////////////
//
// with g++ version:
//      - (tdm64-1) 5.1.0 on Windows
//      - (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609 on Ubuntu 16.04
// >> g++ -std=c++14 -Wall -march=native -O2 array_bench.cpp -o array_bench
//
// c-style stack array duration=0ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.124ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.648ms
// c-style heap array size=8B
//
// std::array duration=1ms
// std::array size=400,000B
//
// std::vector duration=0.402ms
// std::vector size=400,000B
//
// std::deque duration=0.234ms
// std::deque size=400,000B
//
// std::queue duration=0.304ms
// std::queue size=400,000
//
//////////////////////////////////////////////////////////////////////////////////////////

ノート

  • 平均10回の実行で組み立てられます。
  • 最初にstd::sort()も使用してテストを実行しました(コメントアウトされていることがわかります)が、相対的な有意差がないため後で削除しました。

私の結論と発言

  • グローバルcスタイル配列がヒープcスタイル配列とほぼ同じ時間を要することに注意してください。
  • すべてのテストの中で、std::arrayの連続した実行の間の時間変動に顕著な安定性があることに気付きましたが、他のテストでは特にstd ::データ構造体が大きく異なります
  • O3最適化では、注目すべき時間差は示されませんでした
  • Windows cl(-O2なし)およびg ++(Win/Linux no -O2、-march = native)で最適化を削除すると、時間が大幅に増加します。特にstd :: data構造体の場合。 MSVSではg ++よりも全体的に長い時間ですが、std::arrayおよびcスタイルの配列はWindowsで最適化なしで高速です
  • g ++は、Microsoftのコンパイラよりも高速なコードを生成します(明らかにWindows上でも高速に実行されます)。

評決

もちろん、これは最適化されたビルドのコードです。そして、質問はstd::vectorに関するものだったので、そうです!プレーン配列よりも遅い(最適化/非最適化)。しかし、ベンチマークを行っているときは、当然、最適化されたコードを生成する必要があります。

私にとってショーのスターはstd::arrayでしたが。

1
Nikos

VectorのPush_backメソッドの仕組みは次のとおりです。

  1. ベクトルは、初期化時にX個のスペースを割り当てます。
  2. 以下に述べるように、アイテムの現在の基になる配列に空きがあるかどうかをチェックします。
  3. Push_back呼び出しでアイテムのコピーを作成します。

Push_back Xアイテムを呼び出した後:

  1. ベクトルは、kXのスペースを2番目の配列に再割り当てします。
  2. 最初の配列のエントリを2番目の配列にコピーします。
  3. 最初の配列を破棄します。
  4. KXエントリに達するまで、2番目の配列をストレージとして使用します。

繰り返す。 reservingスペースでない場合は、間違いなく遅くなります。それ以上に、アイテムをコピーするのが高価な場合、そのような「プッシュバック」はあなたを生き生きと食べさせます。

vector対配列については、他の人々に同意する必要があります。リリースで実行し、最適化をオンにして、Microsoftの友好的な人々がそれを#@%$ ^しないように、いくつかのフラグを追加します。

もう1つ、サイズを変更する必要がない場合は、Boost.Arrayを使用します。

1
wheaties

より良いベンチマーク(私は思う...)、最適化によるコンパイラはコードを変更できます。なぜなら、割り当てられたベクトル/配列の結果はどこでも使用されないからです。結果:

$ g++ test.cpp -o test -O3 -march=native
$ ./test 
UseArray inner completed in 0.652 seconds
UseArray completed in 0.773 seconds
UseVector inner completed in 0.638 seconds
UseVector completed in 0.757 seconds
UseVectorPushBack inner completed in 6.732 seconds
UseVectorPush completed in 6.856 seconds
The whole thing completed in 8.387 seconds

コンパイラ:

gcc version 6.2.0 20161019 (Debian 6.2.0-9)

CPU:

model name  : Intel(R) Core(TM) i7-3630QM CPU @ 2.40GHz

そしてコード:

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
    public:
        TestTimer(const std::string & name) : name(name),
            start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
        {
        }

        ~TestTimer()
        {
            using namespace std;
            using namespace boost;

            posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
            posix_time::time_duration d = now - start;

            cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
                " seconds" << endl;
        }

    private:
        std::string name;
        boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }

    unsigned char r, g, b;
};

void UseVector(std::vector<std::vector<Pixel> >& results)
{
    TestTimer t("UseVector inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel>& pixels = results.at(i);
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack(std::vector<std::vector<Pixel> >& results)
{
    TestTimer t("UseVectorPushBack inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel>& pixels = results.at(i);
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.Push_back(Pixel(255, 0, 0));
    }
}

void UseArray(Pixel** results)
{
    TestTimer t("UseArray inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        results[i] = pixels;

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        // free(pixels);
    }
}

void UseArray()
{
    TestTimer t("UseArray");
    Pixel** array = (Pixel**)malloc(sizeof(Pixel*)* 1000);
    UseArray(array);
    for(int i=0;i<1000;++i)
        free(array[i]);
    free(array);
}

void UseVector()
{
    TestTimer t("UseVector");
    {
        std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
        UseVector(vector);
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPush");
    {
        std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
        UseVectorPushBack(vector);
    }
}


int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorPushBack();

    return 0;
}
1

私はC++の専門家ではないと言わざるを得ません。しかし、いくつかの実験結果を追加するには:

コンパイル:gcc-6.2.0/bin/g ++ -O3 -std = c ++ 14 vector.cpp

機械:

Intel(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz 

OS:

2.6.32-642.13.1.el6.x86_64

出力:

UseArray completed in 0.167821 seconds
UseVector completed in 0.134402 seconds
UseConstructor completed in 0.134806 seconds
UseFillConstructor completed in 1.00279 seconds
UseVectorPushBack completed in 6.6887 seconds
The whole thing completed in 8.12888 seconds

ここで私が奇妙に感じるのは、「UseConstructor」と比較した「UseFillConstructor」のパフォーマンスだけです。

コード:

void UseConstructor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension*dimension);
        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}


void UseFillConstructor()
{
    TestTimer t("UseFillConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension*dimension, Pixel(255,0,0));
    }
}

そのため、提供される追加の「値」によってパフォーマンスが大幅に低下します。これは、コンストラクタをコピーするための複数の呼び出しによるものだと思います。しかし...

コンパイル:

gcc-6.2.0/bin/g++ -std=c++14 -O vector.cpp

出力:

UseArray completed in 1.02464 seconds
UseVector completed in 1.31056 seconds
UseConstructor completed in 1.47413 seconds
UseFillConstructor completed in 1.01555 seconds
UseVectorPushBack completed in 6.9597 seconds
The whole thing completed in 11.7851 seconds

そのため、この場合、gccの最適化は非常に重要ですが、値がデフォルトとして提供されている場合はあまり役に立ちません。これは、私の授業料に反しています。願わくば、どのベクトル初期化形式を選択するかが新しいプログラマーの助けになることを願っています。

0
user2189731

ベクトル(およびsmart_ptr)は、生の配列(および生のポインター)の上に追加される薄いレイヤーにすぎないことを言及したいだけです。実際、連続メモリ内のベクトルのアクセス時間は配列よりも高速です。次のコードは、ベクトルと配列の初期化とアクセスの結果を示しています。

#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>
#include <vector>
#define SIZE 20000
int main() {
    srand (time(NULL));
    vector<vector<int>> vector2d;
    vector2d.reserve(SIZE);
    int index(0);
    boost::posix_time::ptime start_total = boost::posix_time::microsec_clock::local_time();
    //  timer start - build + access
    for (int i = 0; i < SIZE; i++) {
        vector2d.Push_back(vector<int>(SIZE));
    }
    boost::posix_time::ptime start_access = boost::posix_time::microsec_clock::local_time();
    //  timer start - access
    for (int i = 0; i < SIZE; i++) {
        index = Rand()%SIZE;
        for (int j = 0; j < SIZE; j++) {

            vector2d[index][index]++;
        }
    }
    boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time();
    boost::posix_time::time_duration msdiff = end - start_total;
    cout << "Vector total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
    msdiff = end - start_acess;
    cout << "Vector access time: " << msdiff.total_milliseconds() << "milliseconds.\n"; 


    int index(0);
    int** raw2d = nullptr;
    raw2d = new int*[SIZE];
    start_total = boost::posix_time::microsec_clock::local_time();
    //  timer start - build + access
    for (int i = 0; i < SIZE; i++) {
        raw2d[i] = new int[SIZE];
    }
    start_access = boost::posix_time::microsec_clock::local_time();
    //  timer start - access
    for (int i = 0; i < SIZE; i++) {
        index = Rand()%SIZE;
        for (int j = 0; j < SIZE; j++) {

            raw2d[index][index]++;
        }
    }
    end = boost::posix_time::microsec_clock::local_time();
    msdiff = end - start_total;
    cout << "Array total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
    msdiff = end - start_acess;
    cout << "Array access time: " << msdiff.total_milliseconds() << "milliseconds.\n"; 
    for (int i = 0; i < SIZE; i++) {
        delete [] raw2d[i];
    }
    return 0;
}

出力は次のとおりです。

    Vector total time: 925milliseconds.
    Vector access time: 4milliseconds.
    Array total time: 30milliseconds.
    Array access time: 21milliseconds.

したがって、適切に使用すれば速度はほぼ同じになります。 (reserve()またはresize()を使用して他の人が述べたように)。

0
Charles Chow

ちなみに、ベクトルを使用したクラスの表示速度は、intなどの標準型でも遅くなります。マルチスレッドコードは次のとおりです。

#include <iostream>
#include <cstdio>
#include <map>
#include <string>
#include <typeinfo>
#include <vector>
#include <pthread.h>
#include <sstream>
#include <fstream>
using namespace std;

//pthread_mutex_t map_mutex=PTHREAD_MUTEX_INITIALIZER;

long long num=500000000;
int procs=1;

struct iterate
{
    int id;
    int num;
    void * member;
    iterate(int a, int b, void *c) : id(a), num(b), member(c) {}
};

//fill out viterate and piterate
void * viterate(void * input)
{
    printf("am in viterate\n");
    iterate * info=static_cast<iterate *> (input);
    // reproduce member type
    vector<int> test= *static_cast<vector<int>*> (info->member);
    for (int i=info->id; i<test.size(); i+=info->num)
    {
        //printf("am in viterate loop\n");
        test[i];
    }
    pthread_exit(NULL);
}

void * piterate(void * input)
{
    printf("am in piterate\n");
    iterate * info=static_cast<iterate *> (input);;
    int * test=static_cast<int *> (info->member);
    for (int i=info->id; i<num; i+=info->num) {
        //printf("am in piterate loop\n");
        test[i];
    }
    pthread_exit(NULL);
}

int main()
{
    cout<<"producing vector of size "<<num<<endl;
    vector<int> vtest(num);
    cout<<"produced  a vector of size "<<vtest.size()<<endl;
    pthread_t thread[procs];

    iterate** it=new iterate*[procs];
    int ans;
    void *status;

    cout<<"begining to thread through the vector\n";
    for (int i=0; i<procs; i++) {
        it[i]=new iterate(i, procs, (void *) &vtest);
    //  ans=pthread_create(&thread[i],NULL,viterate, (void *) it[i]);
    }
    for (int i=0; i<procs; i++) {
        pthread_join(thread[i], &status);
    }
    cout<<"end of threading through the vector";
    //reuse the iterate structures

    cout<<"producing a pointer with size "<<num<<endl;
    int * pint=new int[num];
    cout<<"produced a pointer with size "<<num<<endl;

    cout<<"begining to thread through the pointer\n";
    for (int i=0; i<procs; i++) {
        it[i]->member=&pint;
        ans=pthread_create(&thread[i], NULL, piterate, (void*) it[i]);
    }
    for (int i=0; i<procs; i++) {
        pthread_join(thread[i], &status);
    }
    cout<<"end of threading through the pointer\n";

    //delete structure array for iterate
    for (int i=0; i<procs; i++) {
        delete it[i];
    }
    delete [] it;

    //delete pointer
    delete [] pint;

    cout<<"end of the program"<<endl;
    return 0;
}

コードの動作は、ベクトルのインスタンス化がコードの最も長い部分であることを示しています。そのボトルネックを通り抜けたら。残りのコードは非常に高速に実行されます。これは、実行しているスレッドの数に関係なく当てはまります。

ちなみに、本当に異常な数のインクルードは無視してください。このコードを使用してプロジェクトのテストを行っているため、インクルードの数は増え続けています。

0
Zachary Kraus

適切なオプションを使用すると、ベクトルと配列は 同一のasmを生成 になります。これらの場合、どちらの方法でも同じ実行可能ファイルを取得するため、もちろん同じ速度です。

0
Roger Pate

それは、vector :: resize()が通常のメモリ割り当て(mallocによる)よりも多くの処理を行うためです。

コピーコンストラクターにブレークポイントを配置して(ブレークポイントを設定できるように定義してください!)、さらに処理時間がかかります。

0
YeenFei