Використовуючи масиви або std :: вектори в C ++, який розрив у продуктивності?


208

У нашому курсі C ++ вони пропонують більше не використовувати масиви C ++ для нових проектів. Наскільки я знаю, сам Stroustroup пропонує не використовувати масиви. Але чи є суттєві відмінності в роботі?


2
Чому, на вашу думку, існує розрив у ефективності.
Мартін Йорк

99
Тому що зазвичай з кращою функціональністю приходить найгірша продуктивність.
tunnuz

19
Я згоден щодо передчасної оптимізації, але вибір кращого способу зберігання на передньому плані має багато сенсу. Часто в реальному світі код потрібно відправляти і розробляти наступний продукт, а кроки оптимізації ніколи не відбувається.
Мураш

132
Я б хотів, щоб люди перестали кричати "передчасна оптимізація!" всякий раз, коли хтось задає просте запитання, пов'язане з продуктивністю! дайте відповідь на запитання, а не просто ПЕРЕДМОЖНО припускайте, що люди роблять щось передчасно.
d7samurai

4
@ d7samaurai: погодьтеся, я ще не бачив когось спробувати використовуватиint main(int argc, const std::vector<string>& argv)
Марк К Коуан

Відповіді:


189

newСлід уникати використання масивів C ++ з (тобто використання динамічних масивів). Існує проблема, що вам слід відстежувати розмір, і вам потрібно видалити їх вручну та виконувати всілякі господарські роботи.

Використання масивів у стеці також не рекомендується, оскільки у вас немає перевірки діапазону, і передача масиву втратить будь-яку інформацію про його розмір (перетворення масиву в покажчик). Ви повинні використовувати boost::arrayв тому випадку, який обертає масив C ++ у малому класі та забезпечує sizeфункцію та ітератори для його перетворення .

Тепер std :: вектор проти рідних C ++ масивів (взяті з Інтернету):

// Comparison of assembly code generated for basic indexing, dereferencing, 
// and increment operations on vectors and arrays/pointers.

// Assembly code was generated by gcc 4.1.0 invoked with  g++ -O3 -S  on a 
// x86_64-suse-linux machine.

#include <vector>

struct S
{
  int padding;

  std::vector<int> v;
  int * p;
  std::vector<int>::iterator i;
};

int pointer_index (S & s) { return s.p[3]; }
  // movq    32(%rdi), %rax
  // movl    12(%rax), %eax
  // ret

int vector_index (S & s) { return s.v[3]; }
  // movq    8(%rdi), %rax
  // movl    12(%rax), %eax
  // ret

// Conclusion: Indexing a vector is the same damn thing as indexing a pointer.

int pointer_deref (S & s) { return *s.p; }
  // movq    32(%rdi), %rax
  // movl    (%rax), %eax
  // ret

int iterator_deref (S & s) { return *s.i; }
  // movq    40(%rdi), %rax
  // movl    (%rax), %eax
  // ret

// Conclusion: Dereferencing a vector iterator is the same damn thing 
// as dereferencing a pointer.

void pointer_increment (S & s) { ++s.p; }
  // addq    $4, 32(%rdi)
  // ret

void iterator_increment (S & s) { ++s.i; }
  // addq    $4, 40(%rdi)
  // ret

// Conclusion: Incrementing a vector iterator is the same damn thing as 
// incrementing a pointer.

Примітка. Якщо ви виділяєте масиви з newі не виділяєте некласові об'єкти (наприклад, звичайний int) або класи без визначеного користувачем конструктора, і ви не хочете, щоб ваші елементи були ініціалізовані спочатку, використання newвиділених масивів може мати переваги у виконанні, оскільки std::vectorініціалізує всі елементи до значення за замовчуванням (0, наприклад, для int) для побудови (зараховує до @bernie для нагадування).


77
Хто винайшов проклятий синтаксис AT&T? Тільки якби я знав ... :)
Мехрдад Афшарі

4
Це не вірно для компілятора Visual C ++. Але для GCC це так.
Тото

5
Суть у моїй відповіді полягає в тому, що вектор не повинен бути повільнішим, ніж відповідність операцій вказівника. Звичайно, це може бути (це легко досягти, включивши режим налагодження теж) :)
Йоханнес Шауб - ліб

18
+1 для "Індексація вектора - це те саме прокляте, що й індексація вказівника." а також для інших висновків.
Наваз

3
@ Piotr99 Я не збираюся з вами сперечатися, але коли ви вивчаєте складання після вивчення мов вищого рівня, синтаксис Intel просто має набагато більше сенсу, ніж деякі зворотні, префіксні (цифри), суфіксні (інструкції) та незрозумілі (доступ до пам'яті ) характер синтаксису AT&T.
Коул Джонсон

73

Преамбула для мікрооптимізаторів

Пам'ятайте:

"Програмісти витрачають величезну кількість часу на роздуми або занепокоєння швидкості некритичних частин своїх програм. Ці спроби ефективності насправді мають сильний негативний вплив при розгляді налагодження та обслуговування. Ми повинні забути про невелику ефективність, скажімо про 97% часу: передчасна оптимізація - корінь усього зла. Але ми не повинні пропускати свої можливості на критичних 3% ".

(Завдяки метаморфозі за повну цитату)

Не використовуйте масив C замість вектора (чи будь-якого іншого) лише тому, що ви вважаєте, що це швидше, оскільки він повинен бути нижчого рівня. Ви помиляєтесь.

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

Це сказав, що ми можемо повернутися до початкового питання.

Статичний / динамічний масив?

Класи масивів C ++ краще поводяться, ніж низькорівневий масив C, оскільки вони багато що знають про себе, і відповідати на запитання масивів C не можуть. Вони здатні чистити після себе. І що ще важливіше, вони зазвичай записуються за допомогою шаблонів та / або вбудованих даних, а це означає, що те, що виявляється у великій кількості коду в налагодженні, дорівнює чи мало коду, що створюється у складанні випусків, що не означає різниці з їх вбудованою менш безпечною конкуренцією.

Загалом, він поділяється на дві категорії:

Динамічні масиви

Використання вказівника на масив malloc-ed / new-ed буде в кращому випадку таким же швидким, як версія std :: vector, і набагато менш безпечним (див. Пост лаб ).

Тому використовуйте std :: vector.

Статичні масиви

Використання статичного масиву буде в кращому випадку:

Тому використовуйте масив std :: .

Неініціалізована пам'ять

Іноді, використовуючи vectorзамість сирого буфера, виникає видима вартість, оскільки vectorініціалізація буфера при створенні, тоді як код, який він замінює, ні, як зауважив Берні у своїй відповіді .

Якщо це так, то ви можете впоратися з цим, скориставшись unique_ptrзамість vectorабо, якщо випадок не є винятковим у вашій кодовій шкалі, насправді напишіть клас, buffer_ownerякий буде володіти цією пам'яттю, і забезпечить вам легкий і безпечний доступ до нього, в т.ч бонуси, такі як зміна розміру (використання realloc?) або все, що вам потрібно.


1
Дякуємо, що також звертаєтесь до статичних масивів - std :: vector марно, якщо вам не дозволяють динамічно розподіляти пам'ять з міркувань продуктивності.
Том

10
Коли ви говорите "Використання статичного масиву буде в кращому випадку таким же швидким, як і версія boost :: array", це показує, наскільки ви упереджені. Це має бути інше навколо, Boost: масив може бути в кращому випадку швидким, як статичні масиви.
Тото

3
@toto: Це непорозуміння: ви повинні прочитати це як "Використання статичного масиву буде в кращому випадку ((так само швидко, як і версія boost :: array) && (набагато менш безпечно))". Я відредагую пост, щоб уточнити це. До речі, дякую за користь сумнівів.
paercebal

1
що про std :: масив?
paulm

4
Завжди показуйте повну цитату. "Програмісти витрачають величезну кількість часу на роздуми або занепокоєння швидкості некритичних частин своїх програм. Ці спроби ефективності насправді мають сильний негативний вплив при розгляді налагодження та обслуговування. Ми повинні забути про невелику ефективність, скажімо про 97% часу: передчасна оптимізація - це корінь усього зла. Але ми не повинні обходити свої можливості в цих критичних 3% ". Інакше це стає безглуздим звуковим звуком.
метаморфоза

32

Вектори - це масиви під кришкою. Вистава однакова.

Одне місце, де можна зіткнутися з проблемою продуктивності, - це не розмір вектора правильно для початку.

По мірі заповнення вектора він змінює себе розмір, і це може означати, нове розподілення масиву з подальшими n конструкторами копій, а потім приблизно n n викликами деструктора з подальшим видаленням масиву.

Якщо ваша конструкція / руйнування дорога, вам набагато краще зробити вектор правильного розміру для початку.

Існує простий спосіб продемонструвати це. Створіть простий клас, який показує, коли він побудований / знищений / скопійований / призначений. Створіть вектор цих речей і почніть натискати на задній кінець вектора. Коли вектор заповниться, відбудеться каскад активності в міру зміни розміру вектора. Потім спробуйте ще раз з розміром вектора до очікуваної кількості елементів. Ви побачите різницю.


4
Кулон: у вистави такий самий великий O. std :: vector трохи веде бухгалтерію, що, імовірно, коштує невеликої кількості часу. ОТОХ, ви закінчуєте велику частину тієї самої бухгалтерії під час прокатки власних динамічних масивів.
dmckee --- кошеня колишнього модератора

так я зрозумів. Але його питання було головним питанням: в чому полягають відмінності у виконанні ..... Я намагався вирішити це.
EvilTeach

Stc :: вектор Gcc дійсно збільшує потужність один на один, якщо ви викликаєте push_back.
bjhend

3
@bjhend Тоді std::vectorзвуки стандарту gcc не відповідають стандартам? Я вважаю, що стандарт вимагає vector::push_backамортизованої постійної складності, і збільшення потужності на 1 на кожну push_backбуде складати n ^ 2 складності після того, як ви врахуєте реалоки. - припускаючи, що якесь експоненціальне нарощування ємності на, push_backі insert, якщо це не reserveпризведе до щонайбільше постійного збільшення факторних копій векторного вмісту. 1,5 експоненціальний коефіцієнт росту вектора означав би ~ 3x стільки копій, якщо ви цього не зробили reserve().
Якк - Адам Невраумон

3
@bjhend ви помиляєтесь. Стандарт забороняє експоненціальне зростання: у пункті 23.2.3 в пункті 16 сказано: "У таблиці 101 перераховані операції, які передбачені для деяких типів контейнерів послідовностей, але не для інших. Реалізація повинна забезпечувати ці операції для всіх типів контейнерів, показаних у стовпці" контейнер ", і повинні впроваджувати їх так, щоб забирати амортизований постійний час ". (таблиця 101 - це та, у якій є push_back). Тепер перестаньте розповсюджувати FUD. Жодна основна реалізація не порушує цю вимогу. Стандартна бібліотека C ++ від Microsoft зростає з коефіцієнтом 1,5x, а GCC зростає з коефіцієнтом 2x.
Р. Мартіньо Фернандес

27

Щоб відповісти на щось, Мехрдад сказав:

Однак можуть бути випадки, коли вам все-таки потрібні масиви. Під час взаємодії з кодом низького рівня (тобто збіркою) або старими бібліотеками, які потребують масивів, можливо, ви не зможете використовувати вектори.

Не зовсім правда. Вектори чудово деградують у масиви / покажчики, якщо ви використовуєте:

vector<double> vector;
vector.push_back(42);

double *array = &(*vector.begin());

// pass the array to whatever low-level code you have

Це працює для всіх основних реалізацій STL. У наступному стандарті потрібно буде попрацювати (навіть якщо це сьогодні добре).


1
Чинний стандарт не говорить про таке. Це мається на увазі, і реалізується як постійне зберігання. Але стандарт просто говорить, що це контейнер з випадковим доступом (використовуючи ітератори). Наступний стандарт буде явним.
Френк Крюгер

1
& * v.begin () просто застосовує & оператор до результату де-посилання ітератора. Зняття посилань може повернути будь-який тип. Використання адреси оператора може знову повернути будь-який тип. Стандарт не визначає це як вказівник на суміжну область пам'яті.
Френк Крюгер

15
Оригінальний текст стандарту 1998 року цього дійсно не потребував, але в 2003 році був доданий додаток, який стосується цього, тому він дійсно охоплюється Стандартом. biljeutter.wordpress.com/2008/04/07/…
Неманья Трифунович

2
C ++ 03 прямо говорить про те, що &v[n] == &v[0] + nдопустимо, якщо nце входить у діапазон розмірів. Абзац, що містить це твердження, не змінювався з C ++ 11.
bjhend

2
чому б просто не використовувати std :: vector :: data ()?
paulm

15

У вас є ще менше причин використовувати звичайні масиви в C ++ 11.

У природі є 3 види масивів від найшвидшого до найповільнішого, залежно від особливостей, які вони мають (звичайно, якість впровадження може зробити речі дуже швидкими навіть для випадку 3 у списку):

  1. Статичний розмір, відомий під час компіляції. ---std::array<T, N>
  2. Динамічний з розміром, відомий під час виконання та ніколи не змінювався. Типова оптимізація тут полягає в тому, що якщо масив можна виділити безпосередньо в стеку. - Недоступно . Можливо, dynarrayв C ++ TS після C ++ 14. В С є ЛОС
  3. Динамічний та змінює розмір під час виконання. ---std::vector<T>

Для 1. звичайних статичних масивів із фіксованою кількістю елементів використовуйте std::array<T, N>в C ++ 11.

2. Для масивів фіксованого розміру, визначених під час виконання, але це не змінить їх розмір, є обговорення в C ++ 14, але він був переміщений до технічної специфікації та зроблений з C ++ 14 нарешті.

Для 3. std::vector<T> зазвичай просять пам’ять у купі . Це може мати наслідки для продуктивності, хоча ви можете використати std::vector<T, MyAlloc<T>>для покращення ситуації з користувацьким розподільником. Перевага порівняно з T mytype[] = new MyType[n];тим, що ви можете змінити його розмір і те, що він не розкладеться на покажчик, як це роблять звичайні масиви.

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


2
std :: dynarray. Переглянувши коментарі національних органів до n3690, цей компонент бібліотеки було проголосовано з робочого документу C ++ 14 в окрему Технічну специфікацію. Цей контейнер не є частиною проекту C ++ 14 станом на n3797. від en.cppreference.com/w/cpp/container/dynarray
Мохамед Ель-Накіб

1
дуже хороша відповідь. стисла та узагальнююча, але більш детальна, ніж будь-яка.
Мохаммед Ель-Накіб

6

Ідіть зі STL. Немає покарання за виконання. Алгоритми дуже ефективні, і вони добре справляються з обробкою деталей, про які більшість із нас не думає.


5

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


4
враховуючи, що вектор суміжний, все ще досить легко взаємодіяти з бібліотеками, які потребують масивів.
Грег Роджерс

Так, але якщо ви хочете возитися з внутрішніми векторами вектора, переваги у використанні вектора буде менше. До речі, ключове слово було "не може".
Мехрдад Афшарі

3
Є лише один випадок, коли мені відомо, де не можна використовувати вектори: якщо розмір дорівнює 0., тоді & a [0] або & * a.begin () не працюватиме. c ++ 1x виправить це, запровадивши функцію a.data (), яка повертає внутрішній буфер, зберігаючи елементи
Йоханнес Шауб - litb

Конкретний сценарій, який я мав на увазі, коли я писав, що це масиви на основі стека.
Мехрдад Афшарі

1
Вектор взаємозв'язку або будь-який суміжний контейнер з C: vec.data()для даних та vec.size()розміру. Це так просто.
Герман Діаго

5

Про внесок дулі .

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


3

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

В оптимізованому режимі я б очікував, що вектор stl наблизиться до ефективності масиву. Це тому, що багато векторних методів є вписаними.


Це важливо згадати. Профілювання матеріалів налагодження STL відбувається дуже, дуже повільно. І це одна з причин того, чому люди сприймають STL повільно.
Ерік Аронесті

3

Однозначно впливає на ефективність використання std::vectorпроти необробленого масиву, коли потрібно неініціалізований буфер (наприклад, використовувати як призначення для memcpy()). Anstd::vectorБуде форматувати всі його елементи , використовуючи конструктор за замовчуванням. Невисокий масив не буде.

Специфікація c ++ для std:vectorконструктора, який бере countаргумент (це третя форма), говорить:

`Конструює новий контейнер з різних джерел даних, необов'язково використовуючи аллокатор, що постачається користувачем.

3) Конструює контейнер з кількістю вставлених за замовчуванням екземплярів T. Копій не робиться.

Складність

2-3) Лінійна в рахунку

Невисокий масив не несе цієї вартості ініціалізації.

Дивіться також Як я можу уникати std :: vector <> ініціалізувати всі його елементи?


2

Різниця продуктивності між двома дуже залежить від реалізації - якщо порівнювати неправильно реалізований std :: вектор з оптимальною реалізацією масиву, масив виграє, але переверне його і вектор виграє ...

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

При цьому вектор IMHO перемагає в сценарії налагодження з налагодженням STL, оскільки більшість реалізацій STL з належним режимом налагодження може принаймні виділити / спробувати типові помилки, які роблять люди під час роботи зі стандартними контейнерами.

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


1
Я думаю, що для задоволення вимог щодо продуктивності (O (1) пошуку та вставки) вам майже доведеться реалізувати std :: vector <>, використовуючи динамічні масиви. Звичайно, це очевидний спосіб зробити це.
dmckee --- кошеня колишнього модератора

Не тільки вимоги до продуктивності, але й вимога зберігання в суміжних. Погана реалізація вектора покладе занадто багато шарів непрямості між масивом та API. Хороша векторна реалізація дозволить вбудовувати вбудований код, SIMD, який використовується у циклах тощо.
Макс Лібберт

Погана реалізація вектора, як описано, не відповідала б стандарту. Якщо ви хочете непрямий, std::dequeможе бути використаний.
Phil1970

1

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


1

Можливо, існує певний крайній випадок, коли у вас є векторний доступ до вбудованої функції всередині вбудованої функції, коли ви вийшли за рамки того, що компілятор буде вбудований, і це змусить виклик функції. Це було б настільки рідко, що не варто турбуватися - загалом я погодився б з litb .

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


1

Я б заперечив, що головна проблема - це не працездатність, а безпека. Ви можете зробити багато помилок з масивами (розгляньте розмір розміру, наприклад), де вектор врятував би вам біль.


1

Вектори використовують трохи більше пам'яті, ніж масиви, оскільки вони містять розмір масиву. Вони також збільшують розмір програм на жорсткому диску і, можливо, слід пам'яті програм. Ці збільшення невеликі, але можуть мати значення, якщо ви працюєте із вбудованою системою. Хоча більшість місць, де ці відмінності мають значення, - це місця, де ви б використовували C, а не C ++.


2
Якщо це має значення, ви, очевидно, не використовуєте масиви з динамічним розміром, і тому такі масиви не потребують зміни розміру. (Якби вони це зробили, ви б якось зберігали розмір). Тому ви можете також використовувати boost :: масив, якщо я не помиляюся - і що змушує вас сказати, що для цього потрібно десь "зберігати розмір"?
Арафангіон

1

Наступний простий тест:

Масив C ++ проти пояснення тесту на векторну ефективність

суперечить висновкам з "Порівняння коду складання, сформованого для базових операцій індексації, перенаправлення та збільшення кроків на векторах та масивах / покажчиках".

Повинна бути різниця між масивами та векторами. Тест говорить так ... просто спробуйте, код є ...


1

Іноді масиви дійсно кращі, ніж вектори. Якщо ви завжди маніпулюєте набором об'єктів фіксованої довжини, масиви краще. Розглянемо такі фрагменти коду:

int main() {
int v[3];
v[0]=1; v[1]=2;v[2]=3;
int sum;
int starttime=time(NULL);
cout << starttime << endl;
for (int i=0;i<50000;i++)
for (int j=0;j<10000;j++) {
X x(v);
sum+=x.first();
}
int endtime=time(NULL);
cout << endtime << endl;
cout << endtime - starttime << endl;

}

де векторна версія X

class X {
vector<int> vec;
public:
X(const vector<int>& v) {vec = v;}
int first() { return vec[0];}
};

і версія масиву X є:

class X {
int f[3];

public:
X(int a[]) {f[0]=a[0]; f[1]=a[1];f[2]=a[2];}
int first() { return f[0];}
};

Версія масиву буде main () буде швидшою, оскільки ми уникаємо накладних витрат "нового" кожного разу у внутрішньому циклі.

(Цей код був розміщений мною на comp.lang.c ++).


1

Якщо ви використовуєте вектори для представлення багатовимірної поведінки, є ефективність.

Чи 2d + вектори спричиняють показник ефективності?

Суть полягає в тому, що є невелика кількість накладних витрат з кожним підвектором, що містить інформацію про розмір, і не обов’язково буде серіалізація даних (як це відбувається з багатовимірними масивами c). Ця відсутність серіалізації може запропонувати більше, ніж можливості мікрооптимізації. Якщо ви робите багатовимірні масиви, можливо, найкраще просто розширити std :: vector і прокатати власну функцію get / set / resize бітів.


0

Якщо припустити масив фіксованої довжини (наприклад, int* v = new int[1000];порівняно std::vector<int> v(1000);з розміром, vякий фіксується 1000), єдине врахування продуктивності, яке дійсно має значення (або принаймні важливе для мене, коли я опинився в подібній дилемі) - це швидкість доступу до елемент. Я шукав векторний код STL, і ось що я знайшов:

const_reference
operator[](size_type __n) const
{ return *(this->_M_impl._M_start + __n); }

Цю функцію, безумовно, підкреслить компілятор. Отже, поки єдине, що ви плануєте зробити, vце отримати доступ до його елементів operator[], здається, що насправді не повинно бути різниці в продуктивності.

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