Warum ist push_back langsamer als operator [] für einen vorher zugewiesenen Vektor?

Gerade lese ich diesen blog http://lemire.me/blog/archives/2012/06/20/do-not-waste-time-with-stl-vectors/ Vergleich der Leistung von operator[] Zuordnung und push_back auf Speicher vorreserviert std::vector und ich beschlossen, es zu versuchen mich. Die Bedienung ist einfach:

//for vector
bigarray.reserve(N);

//START TIME TRACK
for(int k = 0; k < N; ++k)
   //for operator[]:
   //bigarray[k] = k;
   //for push_back
   bigarray.push_back(k);
//END TIME TRACK

//do some dummy operations to prevent compiler optimize
long sum = accumulate(begin(bigarray), end(array),0 0);

Und hier ist das Ergebnis:

 ~/t/benchmark> icc 1.cpp -O3 -std=c++11
 ~/t/benchmark> ./a.out
[               1.cpp:   52]     0.789123s  --> C++ new
[               1.cpp:   52]     0.774049s  --> C++ new
[               1.cpp:   66]     0.351176s  --> vector
[               1.cpp:   80]     1.801294s  --> reserve + push_back
[               1.cpp:   94]     1.753786s  --> reserve + emplace_back
[               1.cpp:  107]     2.815756s  --> no reserve + push_back
 ~/t/benchmark> clang++ 1.cpp -std=c++11 -O3
 ~/t/benchmark> ./a.out
[               1.cpp:   52]     0.592318s  --> C++ new
[               1.cpp:   52]     0.566979s  --> C++ new
[               1.cpp:   66]     0.270363s  --> vector
[               1.cpp:   80]     1.763784s  --> reserve + push_back
[               1.cpp:   94]     1.761879s  --> reserve + emplace_back
[               1.cpp:  107]     2.815596s  --> no reserve + push_back
 ~/t/benchmark> g++ 1.cpp -O3 -std=c++11
 ~/t/benchmark> ./a.out
[               1.cpp:   52]     0.617995s  --> C++ new
[               1.cpp:   52]     0.601746s  --> C++ new
[               1.cpp:   66]     0.270533s  --> vector
[               1.cpp:   80]     1.766538s  --> reserve + push_back
[               1.cpp:   94]     1.998792s  --> reserve + emplace_back
[               1.cpp:  107]     2.815617s  --> no reserve + push_back

Für alle Compiler vector mit operator[] ist viel schneller als raw-pointer mit operator[]. Dies führte zu der ersten Frage: warum? Die zweite Frage ist, habe ich bereits "reserviert", die Erinnerung, warum also opeator[] ist schneller?

Die nächste Frage ist, da der Arbeitsspeicher bereits zugewiesen ist, warum push_back ist-mal langsamer als operator[]?

Den test-code ist unten angehängt:

#include <iostream>
#include <iomanip>
#include <vector>
#include <numeric>
#include <chrono>
#include <string>
#include <cstring>

#define PROFILE(BLOCK, ROUTNAME) ProfilerRun([&](){do {BLOCK;} while(0);}, \
        ROUTNAME, __FILE__, __LINE__);

template <typename T>
void ProfilerRun (T&&  func, const std::string& routine_name = "unknown",
                  const char* file = "unknown", unsigned line = 0)
{
    using std::chrono::duration_cast;
    using std::chrono::microseconds;
    using std::chrono::steady_clock;
    using std::cerr;
    using std::endl;

    steady_clock::time_point t_begin = steady_clock::now();

    //Call the function
    func();

    steady_clock::time_point t_end = steady_clock::now();
    cerr << "[" << std::setw (20)
         << (std::strrchr (file, '/') ?
             std::strrchr (file, '/') + 1 : file)
         << ":" << std::setw (5) << line << "]   "
         << std::setw (10) << std::setprecision (6) << std::fixed
         << static_cast<float> (duration_cast<microseconds>
                                (t_end - t_begin).count()) / 1e6
         << "s  --> " << routine_name << endl;

    cerr.unsetf (std::ios_base::floatfield);
}

using namespace std;

const int N = (1 << 29);

int routine1()
{
    int sum;
    int* bigarray = new int[N];
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "C++ new");
    sum = std::accumulate (bigarray, bigarray + N, 0);
    delete [] bigarray;
    return sum;
}

int routine2()
{
    int sum;
    vector<int> bigarray (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "vector");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

int routine3()
{
    int sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.push_back (k);
    }, "reserve + push_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

int routine4()
{
    int sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.emplace_back(k);
    }, "reserve + emplace_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

int routine5()
{
    int sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.push_back (k);
    }, "no reserve + push_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}


int main()
{
    long s0 = routine1();
    long s1 = routine1();
    long s2 = routine2();
    long s3 = routine3();
    long s4 = routine4();
    long s5 = routine5();
    return int (s1 + s2);
}
Kommentar zu dem Problem
Dein code ist verbuggt, über operator[] auf " zero sized vector (auch wenn der Speicher reserviert wurde mit reserve) ist kein gültiges C++. Nur nicht Abstürzen, denn der Vektor ist ein primitiver Typ. Kommentarautor: john
@john, Er ist nur die Zuordnung der Werte (das ist tatsächlich setzen, um gültige Eingaben), so dass die Regel gilt hier nicht. Das heißt, die size wird nicht ordnungsgemäß aktualisiert werden, so dass der rest der Vektor der Funktionen nicht richtig funktionieren, es sei denn, Sie rufen Sie resize. Kommentarautor: Zac Howland
Durch änderung des reserve resize in der routine 4, bekomme ich die gleichen Zeiten wie bei routine 2 (!). (Ja, es bekommt schneller um eine Größenordnung.) Kommentarautor: dyp
Einfügen eines memset(bigarray, 0, sizeof(int)*N); in routine1 ermöglicht es, so schnell wie routine2. Ich glaube, der Grund, warum routine2 ursprünglich wurde schneller als routine1 wird ziemlich überraschend; ich wage eine Vermutung: einige spezielle Operationen werden ausgeführt, um führen Sie die Schleife, und Sie können oder nicht verwendet werden, die in der push_back Fällen, die einen Beitrag zu Ihrer Langsamkeit. Kommentarautor: dyp
now() Einführung einer Anweisung Neuordnung Zaun? Dh, können die Anweisungen inside das Profil verschoben werden, außerhalb davon, oder Umgekehrt, durch den compiler? Kommentarautor: Yakk - Adam Nevraumont

InformationsquelleAutor der Frage xis | 2013-11-23

Schreibe einen Kommentar