Начальная емкость вектора в C ++


92

Что из capacity()того, 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
Нет никаких гарантий, но я бы серьезно сомневался в качестве любой реализации, которая выделяет память, без моего запроса.
Майк Сеймур,

2
@MikeSeymour Не согласен. Действительно высокопроизводительная реализация может содержать небольшой встроенный буфер, и в этом случае установка начального capacity () на это имеет смысл.
alastair

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

Ответы:


74

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

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
Валентин Мерсье

Практически везде вы обнаружите, что для определения скорости почти всегда рекомендуется просто использовать вектор, поэтому, если вы делаете что-либо, что связано с разреженными данными ...
Эндрю

@ Андрей, с чего они должны были начать? выделение чего-либо означало бы просто тратить время на выделение и освобождение этой памяти, если программист хочет зарезервировать больше, чем установлено по умолчанию. Если вы предполагаете, что они должны начинаться с 1, он все равно выделит это, как только кто-то выделит 1.
Puddle

@Puddle Вы читаете между строк вместо того, чтобы принимать это за чистую монету. Признак того, что это не сарказм, - это слово «умный», а также мой второй комментарий, в котором упоминаются скудные данные.
Эндрю

@Andrew О, хорошо, ты был достаточно рад, что они начали с нуля. Зачем вообще комментировать это в шутливой форме?
Puddle

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 было неопределенным поведением конструировать, 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.