Початкова ємність вектора в C ++


90

Що таке capacity()a, std::vectorякий створений за допомогою конструктора за замовчуванням? Я знаю, що size()нуль. Чи можемо ми стверджувати, що сконструйований за замовчуванням вектор не викликає виділення купи пам'яті?

Таким чином можна було б створити масив з довільним резервом, використовуючи єдине виділення, наприклад std::vector<int> iv; iv.reserve(2345);. Скажімо, з якихось причин я не хочу запускати size()2345.

Наприклад, на Linux (g ++ 4.4.5, ядро ​​2.6.32 amd64)

#include <iostream>
#include <vector>

int main()
{
  using namespace std;
  cout << vector<int>().capacity() << "," << vector<int>(10).capacity() << endl;
  return 0;
}

надруковані 0,10. Це правило чи це залежить від постачальника STL?


7
Стандарт не вказує нічого про початкову ємність вектора, але більшість реалізацій використовують 0.
Містер Анубіс,

11
Гарантії немає, але я б серйозно поставив під сумнів якість будь-якої реалізації, яка виділяє пам’ять, не вимагаючи жодної.
Mike Seymour,

2
@MikeSeymour Не погоджуюсь. Насправді високоефективна реалізація може містити невеликий вбудований буфер, і в цьому випадку встановлення початкової ємності () до цього має сенс.
alastair

6
@alastair При використанні swapвсі ітератори та посилання залишаються дійсними (крім end()с). Це означає, що вбудований буфер неможливий.
Notinlist

Відповіді:


73

Стандарт не визначає, яким capacityповинен бути початковий код контейнера, тому ви покладаєтесь на реалізацію. Загальна реалізація започаткує потужність з нуля, але немає гарантії. З іншого боку, немає можливості покращити свою стратегію, std::vector<int> iv; iv.reserve(2345);тому дотримуйтесь її.


1
Я не купую останню заяву. Якщо ви спочатку не можете покластися на ємність 0, ви можете реструктуризувати свою програму, щоб ваш вектор мав початковий розмір. Це зменшило б половину кількості запитів кучі-пам'яті (від 2 до 1).
bitmask

4
@bitmask: Будучи практичним: чи знаєте ви про якусь реалізацію, де вектор виділяє пам'ять у конструкторі за замовчуванням? Це не гарантується стандартом, але, як зазначає Майк Сеймур, ініціювання розподілу без необхідності мало б поганий запах щодо якості впровадження .
Девід Родрігес - dribeas

3
@ DavidRodríguez-dribeas: Справа не в цьому. Припущення було: "ви не можете зробити краще, ніж ваша поточна стратегія, тому не турбуйтеся, чи не може бути дурних реалізацій". Якби передумова була "таких реалізацій немає, тож не турбуйся", я б купив її. Висновок буває правдивим, але наслідки не працюють. Вибачте, можливо, я збираю ніт.
bitmask

3
@bitmask Якщо існує реалізація, яка виділяє пам'ять для побудови за замовчуванням, виконання того, що ви сказали, зменшить кількість виділень удвічі. Але vector::reserveце не те саме, що вказати початковий розмір. Конструктори вектора, які приймають початкове значення розміру / копію, ініціалізують nоб'єкти і, отже, мають лінійну складність. OTOH, резерв виклику означає лише копіювання / переміщення size()елементів, якщо спрацьовує перерозподіл. На порожньому векторі немає чого копіювати. Тому останнє може бути бажаним, навіть якщо реалізація виділяє пам’ять для побудованого за замовчуванням вектора.
Преторіан,

4
@bitmask, якщо ви стурбовані розподілом до цього ступеня, вам слід поглянути на реалізацію вашої конкретної стандартної бібліотеки і не покладатися на спекуляції.
Марк Ренсом,

36

Реалізації пам’яті std :: vector суттєво відрізняються, але всі, з якими я стикався, починаються з 0.

Наступний код:

#include <iostream>
#include <vector>

int main()
{
  using namespace std;

  vector<int> normal;
  cout << normal.capacity() << endl;

  for (unsigned int loop = 0; loop != 10; ++loop)
  {
      normal.push_back(1);
      cout << normal.capacity() << endl;
  }

  cin.get();
  return 0;
}

Дає такий результат:

0
1
2
4
4
8
8
8
8
16
16

згідно з GCC 5.1 та:

0
1
2
3
4
6
6
9
9
9
13

згідно MSVC 2013.


3
Це настільки недооцінено @Andrew
Valentin Mercier

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

@Andrew, з чого вони повинні були це розпочати? розподіл чого-небудь міг би просто витратити час на виділення та вивільнення цієї пам'яті, якщо програміст хоче зарезервувати більше, ніж за замовчуванням. якщо ви припускаєте, що вони повинні починати з 1, воно призначить це, як тільки хтось все одно виділить 1.
Калюжа,

@Puddle Ви читаєте між рядками замість того, щоб приймати це за номінал. Підказкою того, що це не сарказм, є слово "розумний", а також мій другий коментар, в якому згадуються розріджені дані.
Ендрю

@Andrew О, добре, ви відчули достатнє полегшення, вони почали це в 0. Навіщо взагалі жартувати про це?
Калюжа,

7

Наскільки я зрозумів стандарт (хоча я насправді не міг назвати посилання), інстанція контейнера та розподіл пам'яті навмисно роз'єднані з поважних причин. Тому у вас є окремі окремі дзвінки

  • constructor щоб створити сам контейнер
  • reserve() попередньо виділити відповідний великий блок пам'яті для розміщення принаймні (!) заданої кількості об'єктів

І в цьому є великий сенс. Єдине право існувати для reserve()- це дати вам можливість кодувати навколо можливо дорогих перерозподілів при зростанні вектора. Для того, щоб бути корисним, ви повинні знати кількість предметів, які потрібно зберігати, або, принаймні, потрібно мати змогу зробити освічені здогади. Якщо цього не зробити, вам краще триматися подалі, reserve()оскільки ви просто зміните перерозподіл на марну пам'ять.

Тож склавши все разом:

  • Стандарт навмисно не вказує конструктор, який дозволяє попередньо виділити блок пам'яті для певної кількості об'єктів (що було б принаймні більш бажаним, ніж виділення конкретної реалізації, фіксованого "чогось" під капотом).
  • Розподіл не повинен бути неявним. Отже, для попереднього розподілу блоку вам потрібно зробити окремий дзвінок, reserve()і це не повинно знаходитись у тому самому місці будівництва (може, звичайно, пізніше, після того, як ви дізнаєтесь про необхідний розмір для розміщення)
  • Таким чином, якщо вектор завжди буде попередньо розподіляти блок пам’яті реалізації визначеного розміру, це буде перешкоджати запланованій роботі reserve(), чи не так?
  • Яка була б перевага попереднього розподілу блоку, якщо STL, природно, не може знати цільового призначення та очікуваного розміру вектора? Це буде досить безглуздо, якщо не контрпродуктивно.
  • Натомість правильним рішенням є виділення та реалізація конкретного блоку з першим push_back()- якщо він ще не був явно виділений раніше reserve().
  • У разі необхідного перерозподілу збільшення розміру блоку також є специфічним для реалізації. Я знаю, що векторні реалізації починаються з експоненціального збільшення розміру, але обмежують швидкість приросту на певному максимумі, щоб уникнути втрати величезної кількості пам'яті або навіть її продування.

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


4

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

Зокрема, якщо _ITERATOR_DEBUG_LEVEL! = 0, тоді вектор виділить трохи місця для допомоги у перевірці ітератора.

https://docs.microsoft.com/en-gb/cpp/standard-library/iterator-debug-level

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


Цікаво, що вони порушують noexcept-гарантії (по крайней мере , для C + 17, раніше?): En.cppreference.com/w/cpp/container/vector/vector
Deduplicator

4

Це старе запитання, і всі відповіді тут слушно пояснили точку зору стандарту та спосіб, яким ви можете отримати початкову ємність у портативному режимі std::vector::reserve;

Однак я поясню, чому немає сенсу для будь-якої реалізації STL виділяти пам'ять при побудові std::vector<T>об'єкта ;

  1. std::vector<T> неповних типів;

    До C ++ 17, було невизначеною поведінкою побудови a, std::vector<T>якщо визначення Tвсе ще невідоме на момент інстанціювання. Однак це обмеження було послаблено в C ++ 17 .

    Для того щоб ефективно розподілити пам’ять для об’єкта, потрібно знати його розмір. Починаючи з C ++ 17 і пізніше, у ваших клієнтів можуть бути випадки, коли ваш std::vector<T>клас не знає розмір T. Чи є сенс мати характеристики розподілу пам'яті, що залежать від повноти типу?

  2. Unwanted Memory allocations

    Існує багато, багато, багато разів, коли вам знадобиться модель графіку в програмному забезпеченні. (Дерево - це графік); Ви, швидше за все, змоделюєте його так:

    class Node {
        ....
        std::vector<Node> children; //or std::vector< *some pointer type* > children;
        ....
     };
    

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

    Це лише один приклад, сміливо думайте про більше ...


2

Стандарт не вказує початкове значення ємності, але контейнер STL автоматично зростає, щоб вмістити стільки даних, скільки ви ввели, за умови, що ви не перевищуєте максимальний розмір (використовуйте функцію члена max_size, щоб знати). Для вектора та рядка ріст обробляється realloc, коли потрібно більше місця. Припустимо, ви хочете створити вектор із значенням 1-1000. Без використання резерву код зазвичай призводить до від 2 до 18 перерозподілів протягом наступного циклу:

vector<int> v;
for ( int i = 1; i <= 1000; i++) v.push_back(i);

Зміна коду для використання резерву може призвести до 0 розподілів протягом циклу:

vector<int> v;
v.reserve(1000);

for ( int i = 1; i <= 1000; i++) v.push_back(i);

Приблизно сказати, векторна та струнна ємність зростає в 1,5 - 2 рази кожного разу.

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