Чи std :: вектор набагато повільніше, ніж звичайні масиви?


212

Я завжди думав, що це загальна мудрість, яка 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 0зменшує UseVectorвдвічі (довівши його до 4 -х секунд). Це справді величезне, ІМО.


23
До деякої версії вектора, коли ви перебуваєте в режимі налагодження, додайте додаткові інструкції, щоб перевірити, чи не маєте ви доступу за межі кінця масиву та подібних матеріалів. Щоб отримати реальні таймінги, ви повинні побудувати в режимі випуску та ввімкнути оптимізацію.
Мартін Йорк

40
Добре, що ви відміряли замість того, щоб вірити твердженням, які ви чули через Інтернет.
П Швед

51
вектор буде реалізований в вигляді масиву. Це не "звичайна мудрість", її правда. Ви виявили, що vectorце масив загального призначення, що змінюється. Вітаю. Як і у всіх інструментах загального призначення, можна придумати спеціалізовані ситуації, коли це неоптимально. Ось чому загальноприйнята мудрість полягає в тому, щоб почати з а vectorта розглянути альтернативи, якщо це необхідно.
Dennis Zickefoose

37
хаха, яка різниця в швидкості "кидання брудного посуду в мийку" та "кидання брудного посуду в мийку і перевірки, чи не зламалися вони"?
Імре Л

9
Щодо принаймні VC2010, здається, головна різниця полягає в тому, що malloc () швидше, ніж розмір (). Видаліть розподіл пам'яті з часу, складіть з _ITERATOR_DEBUG_LEVEL == 0, і результати однакові.
Андреас Магнуссон

Відповіді:


260

Використовуючи наступне:

g ++ -O3 Time.cpp -I <MyBoost>
./a.out
UseArray завершено за 2.196 секунд
UseVector завершено за 4.412 секунд
UseVectorPushBack завершено за 8.017 секунд
. Вся справа завершена за 14.626 секунд

Отже масив удвічі швидший за векторний.

Але після ознайомлення з кодом більш детально це очікується; як ви переходите через вектор двічі та масив лише один раз. Зауважте: коли ви resize()вектором, ви не тільки розподіляєте пам'ять, але і пробігаєте через вектор і викликаєте конструктор на кожному члені.

Трохи переставляючи код, щоб вектор ініціалізував кожен об'єкт один раз:

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

Тепер знову робимо ті ж терміни:

g ++ -O3 Time.cpp -I <MyBoost>
./a.out
UseVector завершено за 2221 секунди

Продуктивність вектора лише трохи гірша, ніж масив. ІМО ця різниця незначна і може бути викликана цілою купою речей, не пов'язаних з тестом.

Я б також взяв до уваги, що ви неправильно ініціалізуєте / знищуєте об'єкт Pixel у UseArrray()методі, оскільки жоден конструктор / деструктор не викликається (це може бути не проблемою для цього простого класу, а чогось трохи складнішого (тобто з покажчиками або членами) з покажчиками) спричинить проблеми.


48
@ kizzx2: reserve()замість цього потрібно використовувати resize(). Це виділяє простір для об'єктів (тобто він змінює ємність вектора), але не створює об'єктів (тобто розмір вектора залишається незмінним).
Джеймс Мак-Нілліс

25
Ви робите 1 000 000 000 доступу до масиву. Різниця в часі - 0,333 секунди. Або різниця 0,000000000333 за доступ до масиву. Припускаючи процесор 2,33 ГГц, як мій, який становить 0,7 стадії конвеєра інструкцій на доступ до масиву. Отже, вектор виглядає так, що він використовує одну додаткову інструкцію за доступ.
Мартін Йорк

3
@James McNellis: Ви не можете просто замінити resize()з reserve(), тому що це не регулює внутрішнє уявлення вектора за його власним розміром, тому наступні операції записи його елементів технічно «писати в кінці минулого» і буде виробляти UB. Хоча на практиці кожна реалізація STL буде "вести себе" з цього приводу, як ви пересинхронізуєте розмір вектора? Якщо ви спробуєте зателефонувати resize() після заповнення вектора, він цілком може перезаписати всі ці елементи зі значеннями, створеними за замовчуванням!
j_random_hacker

8
@j_random_hacker: Не це саме те, що я сказав? Я думав, що мені дуже ясно, що reserveзмінюється лише ємність вектора, а не його розмір.
James McNellis

7
Гаразд, іди фігура. У векторних методах було багато винятків, пов'язаних з винятками. Додавання /EHscдо компіляторів компіляції очистило це, і assign()насправді б'є масив. Так.
Павло Мінаєв

55

Чудове запитання. Я зайшов сюди, розраховуючи знайти просте виправлення, яке б пришвидшило векторні тести. Це не вийшло так, як я очікував!

Оптимізація допомагає, але цього недостатньо. З оптимізацією я все ще бачу різницю між продуктивністю 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

Я спробував malloc()перейти new[]на UseArray, щоб об'єкти були сконструйовані. І змінюється від індивідуального призначення поля до присвоєння екземпляру 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[]пошуку та кешувати посилання на 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

Ні, не відрізняється. Принаймні, це не повільніше. Я думав, що це буде мати ефективність, схожу на №2, де я використав Pixel&посилання.

Висновок

Навіть якщо деякі розумні файли cookie з'ясовують, як зробити векторний цикл настільки ж швидким, як масив, це не говорить добре про поведінку за замовчуванням std::vector. Настільки, що компілятор досить розумний, щоб оптимізувати всю C ++ і створити контейнери STL так само швидко, як і необроблені масиви.

Суть полягає в тому, що компілятор не в змозі оптимізувати неоперативні виклики конструктора за замовчуванням під час використання std::vector. Якщо ви користуєтеся звичайними, new[]це оптимізує їх просто чудово. Але не з std::vector. Навіть якщо ви можете переписати свій код, щоб усунути виклики конструктора, що летить перед мантрою тут: "Компілятор розумніший за вас. STL так само швидкий, як і звичайний C. Не турбуйтеся про це".


2
Знову дякую за те, що фактично запущено код. Іноді легко побитися без причин, коли хтось намагається оскаржити популярну думку.
kizzx2

3
"Настільки, що компілятор досить розумний, щоб оптимізувати всю C ++ і створити контейнери STL так само швидко, як і необроблені масиви." Приємні коментарі. У мене є теорія, що цей "компілятор розумний" - це лише міф - розбір C ++ надзвичайно важкий, а компілятор - лише машина.
kizzx2

3
Я не знаю. Звичайно, він зміг уповільнити тест масиву, але він не прискорив векторний. Я редагував вище, де вилучив конструктори з Pixel і зробив це простою структурою, і вона все ще була повільною. Це погані новини для тих, хто використовує прості типи типу vector<int>.
Джон Кугельман

2
Я б хотіла, щоб я справді могла підтвердити вашу відповідь двічі. Розумні ідеї, які можна спробувати (хоча жоден насправді не працював), про які я навіть не міг придумати!
kizzx2

9
Просто хотів зауважити, що складність розбору C ++ (що шалено складний, так) не має нічого спільного з якістю оптимізації. Останнє зазвичай трапляється на етапах, де результат розбору вже багато разів перетворюється на набагато більш низьке представлення.
Павло Мінаєв

44

Це старе, але популярне питання.

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

У С ++ 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).

Зауважте, що якщо у вас є структура, яка зазвичай вимагає ініціалізації, але ви хочете обробити її після вирощування буфера, ви можете це зробити за допомогою спеціального std::vectorалокатора. Якщо ви хочете потім перенести це у більш нормальне std::vector, я вважаю, що обережне використання allocator_traitsта переосмислення ==може спричинити це, але я не впевнений.


Було б також цікаво подивитися, як тут emplace_backпроти push_back.
Даніель

1
Я не можу відтворити ваші результати. Для компіляції вашого коду clang++ -std=c++11 -O3є UseArray completed in 2.02e-07 secondsі UseVector completed in 1.3026 seconds. Я також додав UseVectorEmplaceBackверсію, яка становить приблизно. 2,5 рази так само швидко UseVectorPushBack.
Даніель

1
Коефіцієнти @daniel - оптимізатор видалив усе з версії масиву. Завжди ризик з мікро-орієнтирами.
Якк - Адам Невраумон

4
так, ти маєш рацію, просто подивився на збірку (або її відсутність) .. Мабуть, мабуть, подумав про це, враховуючи різницю ~ 6448514x! Мені цікаво, чому векторна версія не може зробити таку ж оптимізацію. Це робиться, якщо побудована з розмірами, а не змінена розміром.
Даніель

34

Якщо чесно, ви не можете порівнювати реалізацію C ++ з реалізацією на C, як я б назвав вашу версію malloc. malloc не створює об'єктів - він лише виділяє необроблену пам'ять. Щоб ви потім ставилися до цієї пам’яті як до об'єктів, не викликаючи конструктор, поганий C ++ (можливо, недійсний - я залишу це мовним юристам).

Однак це означає , що проста зміна малалока на " 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 можна порівняти з необробленим масивом для доступу, але якщо вам потрібно створити та видалити вектор / масив багато разів, створення складного об'єкта забирає більше часу, ніж створення простого масиву коли конструктор елемента не накреслений. Я не думаю, що це дуже дивно.


3
У вас все ще є вбудований конструктор - конструктор копій.
Ben Voigt

26

Спробуйте з цим:

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, що це набагато більш загальний інструмент, ніж масив. А це означає, що ви повинні розглянути, як ви цим користуєтеся. Він може використовуватися дуже багато різних способів, забезпечуючи функціонал, якого масив навіть не має. І якщо ви використовуєте його "неправильно" для своїх цілей, ви несете великі накладні витрати, але якщо ви правильно їх використовуєте, це, як правило, структура даних з нульовим накладним рівнем. У цьому випадку проблема полягає в тому, що ви окремо ініціалізували вектор (змусивши всі елементи викликати свій замовчувач за замовчуванням), а потім перезаписати кожен елемент окремо з правильним значенням. Для компілятора набагато складніше оптимізувати його, ніж коли ви робите те ж саме з масивом. Ось чому вектор забезпечує конструктор, який дозволяє вам робити саме це:NX.

І коли ви використовуєте це, вектор настільки ж швидкий, як масив.

Так ні, ви не розгромили міф про виставу. Але ви показали, що це правда лише в тому випадку, якщо ви використовуєте вектор оптимально, що теж досить добре. :)

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


1
Я все ще сумніваюся, чи це справедливе порівняння. Якщо ви позбудетеся від внутрішнього циклу, то еквівалентом масиву було б побудувати єдиний об'єкт Pixel, а потім проклеїти його по всьому масиву.
Джон Кугельман

1
Використання new[]виконує ті ж конструкції за замовчуванням, що vector.resize()і, але це набагато швидше. new[]+ внутрішня петля повинна бути такою ж швидкістю, як vector.resize()+ внутрішня петля, але це не так, це майже вдвічі швидше.
Джон Кугельман

@John: Це справедливе порівняння. У вихідному коді масив виділяється , mallocяка не ініціалізує або конструкт нічого, так що це ефективно алгоритм однопрохідний так само , як мій vectorзразок. Що стосується new[]відповіді, очевидно, що обидва вимагають двох пропусків, але у new[]випадку, компілятор може оптимізувати ту додаткову накладну відстань, що вона не робить у vectorвипадку. Але я не бачу, чому цікаво, що відбувається в неоптимальних випадках. Якщо ви дбаєте про продуктивність, ви не пишете такий код.
jalf

@John: Цікавий коментар. Якщо я хотів пропустити весь масив, я думаю, що масив знову є оптимальним рішенням - оскільки я не можу сказати, vector::resize()щоб дати мені безперервний шматок пам'яті, не витрачаючи часу на виклик марних конструкторів.
kizzx2

@ kizzx2: так і ні. Масив, як правило, ініціалізується також у C ++. У C ви використовуєте те, mallocщо не виконує ініціалізацію, але це не працюватиме в C ++ з типами, що не мають POD. Тож у загальному випадку масив C ++ був би таким же поганим. Можливо, питання полягає в тому, що якщо ви збираєтеся виконувати це блискуче часто, чи не повторно б ви використали той самий масив / вектор? А якщо ви це зробите, то ви сплачуєте витрати на "непотрібні конструктори" лише один раз, на самому старті. Справжнє блищення так само швидко відбувається.
jalf

22

Навряд чи було справедливим порівнянням, коли я вперше подивився на ваш код; Я точно думав, що ти не порівнюєш яблука з яблуками. Тому я подумав, давайте, щоб конструктори та деструктори викликали на всіх тестах; а потім порівняти.

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 не є повільнішою з незрозумілих причин, вона робить саме те, що ви просите; сподіваючись, що ви знаєте краще.


3
Судячи з веселощів, які ми (ми з вами та іншими розумними людьми тут) провели, "надія" впровадження STL справді є досить вимогливою: P В основному, ми можемо перебільшувати і робити висновок, що я сподіваюся, що я прочитав та проаналізував усе його джерело код. У будь-якому разі: P
kizzx2,

1
Чудовий! У VS 2013 цей вектор зробив швидше, ніж масиви. Хоча, здається, що для критично важливих для роботи систем потрібно багато тестувати STL, щоб мати можливість ефективно використовувати його.
rozina

7

Спробуйте відключити перевірені ітератори та побудувати у режимі випуску. Ви не повинні бачити великої різниці в продуктивності.


1
Спробував #define _SECURE_SCL 0. Це зробило UseVectorдесь 4 секунди (подібне gccнижче), але все-таки це вдвічі повільніше.
kizzx2

Це майже напевно причина. Корпорація Майкрософт за замовчуванням пропонує нам налагоджувати ітератор як для налагодження, так і для випуску. Ми виявили, що це є першопричиною масового уповільнення після модернізації з 2003 по 2008 рік. Безумовно, один з найбільш згубних готчей візуальної студії.
Дуг Т.

2
@ kizzx2 є ще один макрос для відключення: HAS_ITERATOR_DEBUGGING або щось подібне.
Дуг Т.

Як показують @Martin і мої відповіді, gcc показує ту ж схему, навіть з оптимізацією на -O3.
Джон Кугельман

1
@Doug: Дивлячись на документ, я думаю _HAS_ITERATOR_DEBUGGING, вимкнено збірку версій
kizzx2

4

STL (та інші) GNU, задані vector<T>(n)за замовчуванням, будує прототипний об’єкт T()- компілятор оптимізує порожній конструктор, але тоді копія будь-якого сміття траплятиметься в адресах пам'яті, тепер зарезервованих для об'єкта, приймається STL __uninitialized_fill_n_aux, який циклічно заповнює копії цього об'єкта як значення за замовчуванням у векторі. Отже, "мій" STL - це не циклічне конструювання, а побудова потім циклу / копіювання. Це проти інтуїтивно зрозуміло, але я мав би пам’ятати, коли я коментував нещодавнє запитання про stackoverflow з цього приводу: конструкція / копія може бути ефективнішою для посилань, що перераховуються, тощо.

Так:

vector<T> x(n);

або

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

є - у багатьох реалізаціях STL - щось на зразок:

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

Проблема полягає в тому, що поточне покоління оптимізаторів компілятора, здається, не працює з розуміння того, що temp - це неініціалізований сміття, і не вдається оптимізувати виклик контуру та виклику конструктора копій за замовчуванням. Ви можете з правдивістю стверджувати, що компілятори абсолютно не повинні оптимізувати це, оскільки програміст, що пише вище, має обґрунтовані сподівання, що всі об'єкти будуть однаковими після циклу, навіть якщо сміття (звичайне застереження про 'ідентичний' / operator == 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 ++ від випадкового вчинення чогось такого дорогого переважає надто невеликою реальною вартістю складної копії конструкції.

ОРИГІНАЛЬНИЙ ВІДПОВІДЬ (для довідки / осмислення коментарів): Не випадково. вектор такий же швидкий, як масив, принаймні, якщо ви розумно зарезервували простір. ...


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

-1, там йде моя підтримка на kizzx2. вектор ніколи не буде таким швидким, як масив через додаткову функцію, яку він надає, правило Всесвіту, все має ціну!
YeenFei

Ти пропускаєш, Тоні ... це приклад штучного орієнтиру, але він доводить те, на що він прагне.
Potatoswatter

Троянди зелені, фіалки помаранчеві, низовини гіркі, але відповідь благає їх.
Павло Мінаєв

3

Відповідь Мартіна Йорка мене турбує, оскільки це здається спробою вирішити проблему ініціалізації під килимом. Але він правильний, щоб визначити надлишкову конструкцію за замовчуванням як джерело проблем продуктивності.

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

Для вирішення негайної проблеми, ви, звичайно, можете зателефонувати 2-параметричної версії vector<Pixel>ctor:

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

Це працює, якщо ви хочете ініціалізувати постійне значення, що є звичайним випадком. Але більш загальною є проблема: як можна ефективно ініціалізувати щось складніше, ніж постійне значення?

Для цього ви можете використовувати a 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, але я сподіваюся, що вона буде близькою, оскільки це лише одна конструкція для кожного елемента.


2

Векторні додатково викликають конструктори Pixel.

Кожен з них спричиняє майже мільйон пробігів, які ви призначаєте.

редагувати: тоді є зовнішній цикл на 1 ... 1000, тож зробіть що мільярд дзвінків!

редагуйте 2: було б цікаво переглянути розбирання для випадку UseArray. Оптимізатор міг би оптимізувати все, але він не має іншого ефекту, ніж спалювання процесора.


Ви маєте рацію, але питання полягає в тому, як можна відключити ці безглузді дзвінки ctor? Це не просто для STL-підходу, але важко / негарно для шляху STL.
j_random_hacker

1

Ось як працює push_backметод у векторному:

  1. Вектор виділяє X кількість простору при його ініціалізації.
  2. Як зазначено нижче, він перевіряє, чи є місце в поточному базовому масиві для елемента.
  3. Це робить копію елемента під час виклику push_back.

Після виклику push_backX предметів:

  1. Вектор перерозподіляє kX кількість простору у 2-му масиві.
  2. Копіює записи першого масиву на другий.
  3. Відкидає перший масив.
  4. Тепер використовується другий масив як сховище, поки він не досягне записів kX.

Повторіть. Якщо ви не в reservingкосмосі, це точно буде повільніше. Більше того, якщо копіювати дорого коштувати, тоді такий "push_back", як такий, з'їсть вас живим.

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

Ще одне, якщо вам не потрібно змінювати розмір, використовуйте Boost.Array.


Я розумію, що люди не люблять читати купу коду, коли він розміщений дослівно. Але я користувався так, reserveяк повинен.
kizzx2

Вибачте, я пропустив це. Хіба я нічого іншого, що я там говорив, не корисний?
пшениці

push_backамортизує постійний час. Це здається, що ви описуєте процес O (N). (Кроки 1 і 3 здаються зовсім поза місцем.) push_backОП уповільнює тест на діапазон, щоб побачити, чи потрібно відбуватись перерозподіл, оновлення покажчиків, перевірка проти NULL всередині місця розташування newта інші дрібниці, які зазвичай заглушаються фактична робота програми.
Картопля

Це буде проходити повільніше навіть з тим, reserveяк він все ще повинен зробити цю перевірку (чи потрібно її перерозподіляти) на кожному push_back.
Павло Мінаєв

Всі хороші бали. Те, що я описую, звучить як процес O (N), але це не так, ну не зовсім. Більшість людей, яких я знаю, не розуміють, наскільки vectorце функціональність зміни розміру, це просто "магія". Ось, дозвольте трохи уточнити це.
пшениці

1

Деякі дані профілю (піксель вирівнюється до 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;
    }

Він має таку ж продуктивність, що і масив.


2
На жаль, після "рішення", яке ви дали, pixels.size()буде порушено.
kizzx2

1
це неправильно, ви не можете зателефонувати в резерв, а потім використовувати елементи, ви все одно повинні використовувати push_back для додавання елементів
пауль

1

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

ОС - це Windows 7 64-розрядна (та, яка має ноутбук)

Компілятором є MinGW 4.6.1.

IDE - це Код :: Блоки .

Я перевіряю вихідні коди першого повідомлення.

Результати

О2 оптимізація

UseArray завершено за 2.841 секунду

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

UseVectorPushBack завершився за 11,95 секунди

Вся справа завершена за 17.342 секунди

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

О3 оптимізація

ВикористанняArray завершено за 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 майже однакова.


Навіть я змінюю malloc на новий, у першому тестовому випадку під O3 продуктивність вектора все ще повільніше, ніж масив. Але коли ви зміните значення присвоєння з (255, 0, 0) на (i, i, i), продуктивність вектор і масив майже однакові під O2 та O3, це досить дивно
StereoMatching

Вибачте, я забув змінити вільне на видалення. Після зміни вільного на видалення продуктивність під O3 вектора та масиву зараз однакова, схоже, що аллокатор є основною причиною?
StereoMatching

1

Кращий орієнтир (я думаю ...), компілятор завдяки оптимізаціям може змінити код, оскільки результати виділених векторів / масивів ніде не використовуються. Результати:

$ 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)

ЦП:

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

Я зробив кілька обширних тестів, яких хотів на деякий час. Можна також поділитися цим.

Це моя подвійна завантажувальна машина i7-3770, 16 ГБ Ram, x86_64, на Windows 8.1 та на Ubuntu 16.04. Більше інформації та висновків, зауважень нижче. Тестовано як 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 займає майже стільки ж часу, як і масив у стилі купи
  • З усіх тестів я помітив чудову стабільність в std::array часових коливаннях між послідовними пробіжками, в той час як інші, особливо std :: структури даних дивовижно відрізнялися порівняно
  • Оптимізація O3 не показала помітних відмінностей у часі
  • Видалення оптимізації для Windows cl (no -O2) та g ++ (Win / Linux no-O2, no -march = native) збільшується в рази ЗНАЧЕНО. Зокрема для std :: структур даних. Загалом вищий час на MSVS, ніж g ++, алеstd::array масиви у стилі c швидше в Windows без оптимізації
  • g ++ виробляє швидший код, ніж компілятор microsoft (мабуть, він працює швидше навіть у Windows).

Вирок

Звичайно, це код для оптимізованої збірки. А оскільки питання було про теstd::vector того, так, це багато! повільніше, ніж звичайні масиви (оптимізовані / неоптимізовані). Але коли ви робите еталон, ви, природно, хочете створити оптимізований код.

Зірка шоу для мене хоч і була std::array.


0

Завдяки правильним параметрам, вектори та масиви можуть генерувати однакову зону . У цих випадках вони, звичайно, мають однакову швидкість, тому що ви отримуєте один і той же виконуваний файл в будь-якому випадку.


1
У цьому випадку вони, схоже, не генерують однакові збірки. Зокрема, схоже, немає можливості придушити виклик конструкторам за допомогою векторів. Ви можете посилатися на відповіді тут на цю проблему (вона довго читається, але вона пояснює, чому є різниця в продуктивності у випадках, відмінних від тестового випадку симплетів у посиланні, яке ви пропонували.) (Насправді, мабуть, існує спосіб - - написання користувальницького алокатора STL, як було запропоновано. Особисто я вважаю, що це зайве більше роботи, ніж використання malloc)
kizzx2,

1
@ kizzx2: Те, що вам доведеться піти на таку довжину, щоб використовувати неконструйовані об'єкти, це добре, тому що це помилка 99% (я, можливо, сильно занижуюча) часу. Я прочитав інші відповіді, і розумію, що не звертаюсь до вашої конкретної ситуації (не потрібно, інші відповіді правильні), але я все ж хотів надати вам цей приклад того, як вектори та масиви можуть вести себе точно так само.

@Roger: це чудово! Дякую за посилання
kizzx2

0

До речі, уповільнення вашого бачення в класах із використанням вектора відбувається також із типовими типами, такими як 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

Я просто хочу зазначити, що вектор (і 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.

Тож швидкість буде майже однаковою, якщо її правильно використовувати. (як згадували інші, використовуючи резерв () або розмір ()).


0

Ну тому, що вектор :: resize () обробляє набагато більше, ніж звичайний розподіл пам'яті (malloc).

Спробуйте поставити в конструкторі копій точку розриву (визначте її так, щоб можна було зламати точку!), І на це піде додатковий час обробки.


0

Треба сказати, що я не знавець C ++. Але додати деякі результати експериментів:

компілювати: gcc-6.2.0 / bin / g ++ -O3 -std = c ++ 14 vector.cpp

машина:

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

ОС:

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

Таким чином, додаткова «величина», що надається, уповільнює продуктивність досить багато, що, на мою думку, пов’язане з багаторазовим викликом конструктора копіювання. Але ...

Збірка:

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

Здається, це залежить від прапорів компілятора. Ось базовий код:

#include <chrono>
#include <cmath>
#include <ctime>
#include <iostream>
#include <vector>


int main(){

    int size = 1000000; // reduce this number in case your program crashes
    int L = 10;

    std::cout << "size=" << size << " L=" << L << std::endl;
    {
        srand( time(0) );
        double * data = new double[size];
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of C style heap array:    " << duration << "ms\n";
        delete data;
    }

    {
        srand( 1 + time(0) );
        double data[size]; // technically, non-compliant with C++ standard.
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of C99 style stack array: " << duration << "ms\n";
    }

    {
        srand( 2 + time(0) );
        std::vector<double> data( size );
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of std::vector array:     " << duration << "ms\n";
    }

    return 0;
}

Різні прапори оптимізації дають різні відповіді:

$ g++ -O0 benchmark.cpp 
$ ./a.out 
size=1000000 L=10
Calculation result is 181182
Duration of C style heap array:    118441ms
Calculation result is 181240
Duration of C99 style stack array: 104920ms
Calculation result is 181210
Duration of std::vector array:     124477ms
$g++ -O3 benchmark.cpp
$ ./a.out 
size=1000000 L=10
Calculation result is 181213
Duration of C style heap array:    107803ms
Calculation result is 181198
Duration of C99 style stack array: 87247ms
Calculation result is 181204
Duration of std::vector array:     89083ms
$ g++ -Ofast benchmark.cpp 
$ ./a.out 
size=1000000 L=10
Calculation result is 181164
Duration of C style heap array:    93530ms
Calculation result is 181179
Duration of C99 style stack array: 80620ms
Calculation result is 181191
Duration of std::vector array:     78830ms

Ваші точні результати будуть різними, але це досить типово для моєї машини.


0

На мій досвід, іноді, просто іноді, vector<int>може бути в багато разів повільніше, ніж int[]. Слід пам’ятати, що вектори векторів дуже не схожі int[][]. Оскільки елементи, ймовірно, не є суміжними в пам'яті. Це означає, що ви можете змінити розмір різних векторів всередині основного, але процесор може не в змозі кешувати елементи так само, як у випадку int[][].

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.