Является ли 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 может быть медленнее, для несколько nanosecs" комментарии.

и код, который я использовал:

#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 0 уменьшается UseVector наполовину (доведя его до 4 секунд). Это действительно огромный, ИМО.

20 ответов


исходя из следующего:

время g++ -O3.cpp-I
./а.из
UseArray завершен за 2,196 секунды
UseVector завершен за 4,412 секунды
UseVectorPushBack завершен за 8,017 секунды
Все это закончилось за 14,626 секунды

таким образом, массив в два раза быстрее, чем вектор.

но после просмотра кода более подробно это ожидается; как вы бежите через вектор и массив только один раз. Примечание: когда вы resize() вектор вы не только выделяете память, но и запускаете вектор и вызываете конструктор для каждого члена.

слегка переупорядочить код так, чтобы вектор инициализировал каждый объект только один раз:

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

Теперь делает то же самое время снова:

время g++ -O3.cpp-I
./а.из
UseVector завершен за 2,216 секунды

вектор теперь производительность только немного хуже, чем массив. ИМО эта разница незначительна и может быть вызвана целой кучей вещей, не связанных с тестом.

Я бы также принял во внимание, что вы неправильно инициализируете/уничтожаете объект Pixel в UseArrray() метод, поскольку ни конструктор / деструктор не вызывается (это может не быть проблемой для этого простого класса, но что-то немного более сложное (т. е. с указателями или члены с указателями) вызовет проблемы.


большой вопрос. Я пришел сюда, ожидая найти простое исправление,которое ускорит векторные тесты. Это не совсем так, как я ожидал!

оптимизация помогает, Но этого недостаточно. С оптимизацией я все еще вижу разницу в производительности 2X между UseArray и UseVector. Интересно, что 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

Idea #1-Используйте new[] вместо Мэллок!--26-->

я пытался менять malloc() to new[] в UseArray, чтобы объекты были построены. И переход от индивидуального назначения поля к назначению экземпляра пикселя. О, и переименование переменной внутреннего цикла в 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.

Idea #2-удалить повторные вызовы оператора []

я также попытался избавиться от тройного 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 может оптимизировать построение всех объектов, когда векторы создан. Что произойдет, если мы изменим пиксель:

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

результат: около 10% быстрее. Еще медленнее, чем массив. Хм.

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

Idea #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

нет, ничем не отличается. По крайней мере не медленнее. Я думал, что это будет иметь производительность, подобную #2, где я использовал Pixel& ссылка.

вывод

даже если какой-то умный cookie выясняет, как сделать векторный цикл так же быстро, как массив, это не говорит Хорошо о поведении по умолчанию std::vector. Так много для компилятора, достаточно умного, чтобы оптимизировать все c++ness и сделать контейнеры STL такими же быстрыми, как необработанные массивы.

суть в том, что компилятор не может оптимизировать вызовы конструктора no-op по умолчанию при использовании std::vector. Если вы используете простой new[] оптимизирует их просто отлично. Но не с std::vector. Даже если вы можете переписать свой код, чтобы исключить вызовы конструктора, которые летают перед лицом мантры здесь: "компилятор умнее вас. STL так же быстро, как простой C. Не беспокойтесь об этом."


справедливости ради, вы не можете сравнить реализацию C++ с реализацией C, как я бы назвал вашу версию malloc. malloc не создает объекты - он выделяет только необработанную память. То, что вы затем рассматриваете эту память как объекты без вызова конструктора, является плохим 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

но с небольшим изменением таблицы свою очередь:

пикселей.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 сравнимый с необработанным массивом для доступа, но если вам нужно создать и удалить вектор/массив много раз, создание сложного объекта займет больше времени, чем создание простого массива, когда конструктор элемента не встроен. Не думаю, что это удивительно.


это старый, но популярный вопрос.

на данный момент многие программисты будут работать на 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 имеет две перегрузки. Первый -std::vector<T>::resize(size_t), другой std::vector<T>::resize(size_t, T const&). Это означает, когда вы вызываете resize без второго аргумента он просто строит по умолчанию, и компилятор достаточно умен, чтобы понять, что конструкция по умолчанию ничего не делает, поэтому он пропускает проход через буфер.

(две перегрузки, добавленные для обработки подвижных, конструктивных и не копируемых типов - повышение производительности при работе с неинициализированными данные бонус).

на push_back решение также проверяет fencepost, что замедляет его, поэтому он остается медленнее, чем malloc версия.

видео (я также заменил таймер на chrono::high_resolution_clock).

обратите внимание, что если у вас есть структура, которая обычно требует инициализации, но вы хотите обработать ее после увеличения буфера, вы можете сделать это с помощью custom std::vector аллокатор. Если вы хотите, чтобы затем переместить его в более нормальное std::vector, Я считаю тщательное использование allocator_traits и переопределить == может это и получится, но я не уверен.


попробуйте с помощью этого:

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));
    }
}

я получаю почти ту же производительность, что и с array.

насчет vector это гораздо более общий инструмент, чем массив. И это означает, что вы должны рассмотреть как вы используете его. Он может использоваться по-разному, обеспечивая функциональность, которой массив даже не имеет. И если вы используете его "неправильно" для своей цели, вы несете много накладных расходов, но если вы используете его правильно, это обычно в основном структура данных с нулевыми накладными расходами. В этом случае проблема заключается в том, что вы отдельно инициализировали вектор (вызывая для всех элементов их ctor по умолчанию), а затем перезаписали каждый элемент по отдельности с правильным значением. Это намного труднее для компилятора оптимизировать, чем когда вы делаете то же самое с массивом. Именно поэтому вектор предоставляет конструктор, который позволяет вам сделать именно это: initialize N элементы со значением X.

и когда вы используете это, вектор так же быстро, как массив.

так что нет, вы не разрушили миф о производительности. Но вы показали, что это верно только в том случае, если вы используете вектор оптимально, что тоже довольно хорошо. :)

С другой стороны, это действительно простой использование, которое оказывается самым быстрым. Если вы сравните мой фрагмент кода (одну строку) с ответом Джона Кугельмана, содержащим кучи и кучи настроек и оптимизаций, которые все еще не довольно устранить разницу в производительности, это довольно ясно, что vector довольно хитро, в конце концов. Вам не нужно прыгать через обручи, чтобы получить скорость, равную массиву. Напротив, вы должны использовать самое простое из возможных решений.


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 ' s ООН-инициализации значения.

по сравнению с альтернативой ситуация с 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 реализация не медленнее по какой-то неизвестной причине, она просто делает именно то, что вы просите; надеясь, что вы знаете лучше.


попробуйте отключить проверяемые итераторы и построение в режиме выпуска. Вы не должны видеть большой разницы в производительности.


STL GNU (и другие), учитывая vector<T>(n), по умолчанию создает прототипный объект T() - компилятор оптимизирует пустой конструктор - но тогда копия любого мусора, который оказался в адресах памяти, теперь зарезервированных для объекта, берется STL __uninitialized_fill_n_aux, который циклически заполняет копии этого объекта в качестве значений по умолчанию в векторе. Итак," мой " 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 является неинициализированным мусором и не оптимизирует цикл и вызовы конструктора копирования по умолчанию. Вы можете достоверно утверждать, что компиляторы абсолютно не должны оптимизировать это, так как программист, пишущий выше, имеет разумное ожидание, что все объекты будут идентичны после цикла, даже если мусор (обычные предостережения об "идентичном" /operator== vs memcmp/operator= etc применяются). Компилятор не может иметь никакого дополнительного представления о более широком контексте std:: vector или более позднем использовании данных, которые предложили бы эту оптимизацию безопасный.

это можно сравнить с более очевидной, прямой реализацией:

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

который мы можем ожидать, что компилятор оптимизирует.

чтобы быть немного более явным об оправдании этого аспекта поведения вектора, рассмотрим:

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

ясно, что это большая разница, если мы сделаем 10000 независимых объектов против 10000, ссылающихся на одни и те же данные. Есть разумный аргумент, что преимущество защиты casual C++ пользователи, случайно сделавшие что-то настолько дорогое, перевешивают очень небольшую реальную стоимость трудно оптимизируемой конструкции копирования.

оригинальный ответ (Для справки / смысл комментариев): Никаких шансов. вектор так же быстр, как массив, по крайней мере, если вы зарезервируете пространство разумно. ...


ответ Мартина Йорка беспокоит меня, потому что это похоже на попытку убрать проблему инициализации под ковер. Но он прав, определяя избыточную конструкцию по умолчанию как источник проблем с производительностью.

[EDIT: ответ Мартина больше не предполагает изменения конструктора по умолчанию.]

для непосредственной проблемы под рукой, вы можете, конечно, вызвать 2-параметрическую версию vector<Pixel> ctor вместо этого:

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().

недостатком является то, что логика для построения начальных значений должна быть перемещена в отдельный класс, что менее удобно, чем иметь его на месте (хотя lambdas в C++1x делают это намного приятнее). Также я ожидаю, что это все равно будет не так быстро, как malloc()-версия без STL, но я ожидаю, что она будет близка, так как она делает только одну конструкцию для каждого элемента.


векторные дополнительно вызывают пиксельные конструкторы.

каждый вызывает почти миллион запусков ctor, которые вы синхронизируете.

edit: тогда есть внешний 1...1000 петель, так что сделайте миллиард вызовов ctor!

edit 2: было бы интересно посмотреть разборку для случая UseArray. Оптимизатор может оптимизировать все это, так как он не имеет никакого эффекта, кроме сжигания процессора.


вот как push_back метод векторных произведений:

  1. вектор выделяет X объем пространства при его инициализации.
  2. как указано ниже, он проверяет, есть ли место в текущем базовом массиве для элемента.
  3. он делает копию элемента в вызове push_back.

после вызова push_back X элементы:

  1. вектор перераспределяет объем пространства kX во второй массив.
  2. он копирует записи первого массива на второй.
  3. отбрасывает первый массив.
  4. теперь использует второй массив в качестве хранилища, пока он не достигнет записей kX.

повторить. Если вы не reserving пространство определенно будет медленнее. Более того, если копировать элемент дорого, то "push_back", как это, съест вас заживо.

как к vector по сравнению с массивом, я собираюсь согласиться с другими людьми. Запуск в выпуске, включите оптимизацию и добавьте еще несколько флагов, чтобы дружелюбные люди в Microsoft не #@%$^ это для вас.

еще одна вещь, если вам не нужно изменять размер, используйте Boost.Матрица.


некоторые данные профилировщика (пиксель выровнен до 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

мля

andrey@nv:~$ 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;
    }

он имеет ту же производительность, что и массив.


мой ноутбук Lenova G770 (4 ГБ оперативной памяти).

ОС Windows 7 64-бит (с ноутбука)

компилятор MinGW 4.6.1.

IDE является Код::Блоки.

Я тестирую исходные коды первого сообщения.

результаты

оптимизация O2

UseArray завершен за 2,841 секунды

UseVector завершен за 2,548 секунды

UseVectorPushBack завершено за 11,95 секунды

все это закончилось за 17,342 секунды

система пауза

оптимизация O3

UseArray завершен за 1,452 секунды

UseVector завершен за 2,514 секунды

UseVectorPushBack завершен за 12.967 секунд

все дело завершится в 16.937 секунд

похоже, что производительность vector хуже при оптимизации O3.

Если вы измените цикл на

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

скорость массива и вектора под O2 и O3 почти одинакова.


лучший ориентир (я думаю...), компилятор благодаря оптимизации может изменять код, поскольку результаты выделенных векторов / массивов нигде не используются. Результаты:

$ 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;
}

с правильными параметрами векторы и массивы могут создать идентичный asm. В этих случаях они, конечно же, одинаковы, потому что вы получаете один и тот же исполняемый файл в любом случае.


кстати, замедление вашего видения в классах с использованием вектора также происходит со стандартными типами, такими как 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;
}

поведение из кода показывает, что экземпляр vector является самой длинной частью кода. Как только пробьешь горлышко бутылки. Остальная часть кода работает очень быстро. Это верно независимо от того, сколько потоков вы работаете.

кстати игнорируйте абсолютно безумное количество включений. Я использовал этот код для тестирования вещей для проекта, поэтому количество включений продолжает расти.


Я просто хочу упомянуть, что vector (и 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 ()).


Ну, потому что vector:: resize() делает гораздо больше обработки, чем простое выделение памяти (по malloc).

Попробуйте поместить точку останова в конструктор копирования (определите ее так, чтобы вы могли точку останова!) и идет дополнительное время обработки.


Я должен сказать, что я не эксперт в C++. Но чтобы добавить некоторые результаты экспериментов:

компиляции: gcc-6.2.0/bin / g++ - O3-std=вектор c++14.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

здесь единственное, что мне кажется странным, это производительность " UseFillConstructor "по сравнению с"UseConstructor".

код:

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));
    }
}

таким образом, дополнительное" значение " замедляется производительность довольно большая, что, я думаю, связано с многократным вызовом конструктора копирования. Но...

Compile:

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 очень важна, но она не может помочь вам, когда значение предоставляется по умолчанию. Это, вообще-то, против моего обучения. Надеюсь, это поможет новому программисту при выборе формата инициализации вектора.


Я сделал несколько обширных тестов, которые я хотел на некоторое время. Можно и этим поделиться.

это моя двойная загрузочная машина i7-3770, 16GB Ram, x86_64, на Windows 8.1 и на Ubuntu 16.04. Более подробная информация и выводы, замечания ниже. Протестировано как MSVS 2017, так и g++ (как в Windows, так и в Linux).

Программы Test

#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 (no-O2) и на g++ (Win/Linux no-O2, no-march=native) значительно увеличивает время. Особенно для структур std::data. В целом более высокие времена на MSVS, чем g++, но std::array и C-style массивы быстрее на Windows без оптимизации
  • g++ производит более быстрый код, чем компилятор microsoft (по-видимому, он работает быстрее даже на Windows).

приговором

конечно, это код для оптимизированной сборки. И так как вопрос был о std::vector тогда да !много! медленнее, чем простые массивы (оптимизированные/неоптимизированные). Но когда вы делаете тест, вы, естественно, хотите создать оптимизированный код.

звезда шоу для меня, хотя было std::array.