Подтвердить что ты не робот

Вектор: инициализация или резерв?

Я знаю размер вектора, который является лучшей процедурой для его инициализации?:

вариант 1

vector<int> vec(3); //in .h
vec.at(0)=var1;     //in .cpp
vec.at(1)=var2;     //in .cpp
vec.at(2)=var3;     //in .cpp

опция2

vector<int> vec;     //in .h
vec.reserve(3);     //in .cpp
vec.push_back(var1);     //in .cpp
vec.push_back(var2);     //in .cpp
vec.push_back(var3);     //in .cpp

Я предполагаю, что вариант 2 лучше, чем 1. это? другие варианты?

4b9b3361

Ответ 1

Оба варианта имеют разную семантику, т.е. вы сравниваете яблоки и апельсины.

Первый дает вам вектор n значений, инициализированных по умолчанию, второй вариант резервирует память, но не инициализирует их.

Выберите, что лучше подходит вашим потребностям, то есть "лучше" в определенной ситуации.

Ответ 2

"Лучший" способ:

vector<int> vec = {var1, var2, var3};

доступен с компилятором, совместимым с С++ 11.

Не уверен, что вы имеете в виду, делая вещи в заголовке или файлах реализации. Переменный мир - это не-нет для меня. Если это член класса, то он может быть инициализирован в списке инициализации конструктора.

В противном случае вариант 1 будет обычно использоваться, если вы знаете, сколько элементов вы собираетесь использовать, и значения по умолчанию (0 для int) будут полезны.
Использование at здесь означает, что вы не можете гарантировать, что индекс действителен. Подобная ситуация вызывает тревогу. Несмотря на то, что вы сможете надежно обнаруживать проблемы, определенно проще использовать push_back и перестать беспокоиться о правильном использовании индексов.

В случае варианта 2, как правило, он делает нулевую разницу в производительности, резервируя память или нет, поэтому проще не резервировать *. Если, возможно, если вектор содержит типы, которые очень дороги для копирования (и не обеспечивают быстрое перемещение в С++ 11), или размер вектора будет огромным.


* От Stroustrups Часто задаваемые вопросы по стилю и технике С++:

Люди иногда беспокоятся о стоимости std::vector роста постепенно. Я беспокоился об этом и использовал reserve() для оптимизировать рост. После измерения моего кода и неоднократного проблема с поиском выгод от резерва() в реальном программ, я прекратил использовать его, кроме случаев, когда необходимо избегать недействительность итератора (редкий случай в моем коде). Опять: измерьте до вы оптимизируете.

Ответ 3

Хотя ваши примеры по сути одинаковы, может случиться так, что когда используемый тип не является int выбор за вами. Если ваш тип не имеет конструктора по умолчанию, или если вам все равно придется заново конструировать каждый элемент позже, я бы использовал reserve. Только не попадитесь в ловушку, которую я сделал, и используйте reserve а затем operator[] для инициализации!


Конструктор

std::vector<MyType> myVec(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();

В этом первом случае при использовании конструктора size и numberOfElementsToStart будут равны, а capacity будет больше или равна им.

Думайте о myVec как о векторе, содержащем несколько элементов MyType которым можно получить доступ и изменить их, push_back(anotherInstanceOfMyType) добавит его в конец вектора.


резерв

std::vector<MyType> myVec;
myVec.reserve(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();

При использовании функции reserve size будет равен 0 до тех пор, пока вы не добавите элемент в массив, а capacity будет равна или больше, чем numberOfElementsToStart.

Подумайте о myVec как о пустом векторе, к которому можно добавлять новые элементы, используя push_back без выделения памяти, по крайней мере, для первых элементов numberOfElementsToStart.

Обратите внимание, что push_back() прежнему требует внутренней проверки, чтобы гарантировать, что размер <емкость и увеличивает размер, поэтому вы можете взвесить это по сравнению со стоимостью конструкции по умолчанию.


Инициализация списка

std::vector<MyType> myVec{ var1, var2, var3 };

Это дополнительная опция для инициализации вашего вектора, и хотя она возможна только для очень маленьких векторов, это простой способ инициализировать небольшой вектор с известными значениями. size будет равен количеству элементов, с которыми вы его инициализировали, а capacity будет равна или больше размера. Современные компиляторы могут оптимизировать создание временных объектов и предотвратить ненужное копирование.

Ответ 4

Вариант 2 лучше, так как резерв только резервирует память (3 * sizeof (T)), а первая опция вызывает конструктор базового типа для каждой ячейки внутри контейнера.

Для C-подобных типов он, вероятно, будет таким же.

Ответ 5

В конечном итоге это зависит от использования и количества элементов.

Запустите программу ниже, чтобы понять, как компилятор резервирует пространство:

vector<int> vec;
for(int i=0; i<50; i++)
{
  cout << "size=" << vec.size()  << "capacity=" << vec.capacity() << endl;
  vec.push_back(i);
}

size - количество фактических элементов, а емкость - это фактический размер массива для вектора-символа. На моем компьютере, до 10, оба одинаковы. Но, когда размер составляет 43, емкость составляет 63. В зависимости от количества элементов, может быть, лучше. Например, увеличение емкости может быть дорогостоящим.

Ответ 6

Другой вариант - доверять вашему компилятору (tm) и сделать push_back без вызова reserve. При запуске добавления элементов необходимо выделить некоторое пространство. Возможно, это так же хорошо, как и вы?

"Лучше" иметь более простой код, который выполняет ту же работу.

Ответ 7

Как это устроено

Это зависит от конкретной реализации, однако в общем случае внутренняя структура векторных данных будет иметь указатель на блок памяти, в котором будут находиться элементы. И GCC, и VC++ выделяют по 0 элементов по умолчанию. Таким образом, вы можете думать, что указатель внутренней памяти Vector по умолчанию равен nullptr.

Когда вы вызываете vector<int> vec(N); как и в вашем варианте 1, N объектов создаются с использованием конструктора по умолчанию. Это называется конструктором заполнения.

Когда вы делаете vec.reserve(N); после конструктора по умолчанию, как в варианте 2, вы получаете блок данных, содержащий 3 элемента, но в отличие от варианта 1 объекты не создаются.

Зачем выбирать вариант 1

Если вы знаете, что число элементов будет содержать вектор, и вы можете оставить большинство элементов по умолчанию, тогда вы можете использовать эту опцию.

Зачем выбирать вариант 2

Этот вариант, как правило, лучше двух, поскольку он выделяет только блок данных для будущего использования и фактически не заполняется объектами, созданными из конструктора по умолчанию.

Ответ 8

Так как кажется, прошло 5 лет, а неправильный ответ по-прежнему является принятым, и самый ответный ответ полностью бесполезен (пропустил лес для деревьев), я добавлю реальный ответ.

Метод # 1: мы передаем параметр начального размера в вектор (пусть назовем его n). Это означает, что вектор заполняется элементами n, которые будут инициализированы по умолчанию Например, если вектор имеет значение int s, он будет заполнен нулями n.

Метод # 2: мы сначала создаем пустой вектор. Затем зарезервируем место для элементов n. В этом случае мы никогда не создаем элементы n, и поэтому мы никогда не выполняем инициализацию элементов в векторе. Поскольку мы планируем немедленно перезаписать значения каждого элемента, отсутствие инициализации не принесет нам никакого вреда. С другой стороны, поскольку мы сделали меньше всего, это был бы лучший вариант *.

* лучше - реальное определение: никогда не хуже. Всегда возможно, что интеллектуальный компилятор будет определять, что вы пытаетесь сделать, и оптимизировать его для вас.


Заключение: используйте метод # 2.

Ответ 9

Каким-то образом, ответ без ответа, который является полностью неправильным, остается принятым и большинством голосов в течение ~ 7 лет. Это не вопрос яблок и апельсинов. Это не вопрос, на который нужно отвечать расплывчатыми штампами.

Для простого правила следовать:

Вариант № 1 быстрее... enter image description here enter image description here

... но это, вероятно, не должно быть вашей самой большой проблемой.

Во-первых, разница довольно незначительная. Во-вторых, когда мы запускаем оптимизацию компилятора, разница становится еще меньше. Например, на моем gcc-5.4.0 разница, возможно, тривиальна при запуске оптимизации компилятора уровня 3 (-O3): enter image description here

В общем, я бы рекомендовал использовать метод № 1 всякий раз, когда вы сталкиваетесь с такой ситуацией. Тем не менее, если вы не можете вспомнить, какой из них является оптимальным, вероятно, это не стоит усилий, чтобы выяснить. Просто выберите любой из них и двигайтесь дальше, потому что это вряд ли когда-либо приведет к заметному замедлению работы вашей программы в целом.


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

main.cpp:

/* 
 * Test constructing and filling a vector in two ways: construction with size
 * then assignment versus construction of empty vector followed by push_back
 * We collect dummy sums to prevent the compiler from optimizing out computation
 */

#include <iostream>
#include <vector>

#include "rng.hpp"
#include "timer.hpp"

const size_t kMinSize = 1000;
const size_t kMaxSize = 100000;
const double kSizeIncrementFactor = 1.2;
const int kNumVecs = 10000;

int main() {
  for (size_t mean_size = kMinSize; mean_size <= kMaxSize;
       mean_size = static_cast<size_t>(mean_size * kSizeIncrementFactor)) {
    // Generate sizes from normal distribution
    std::vector<size_t> sizes_vec;
    NormalIntRng<size_t> sizes_rng(mean_size, mean_size / 10.0); 
    for (int i = 0; i < kNumVecs; ++i) {
      sizes_vec.push_back(sizes_rng.GenerateValue());
    }
    Timer timer;
    UniformIntRng<int> values_rng(0, 5);
    // Method 1: construct with size, then assign
    timer.Reset();
    int method_1_sum = 0;
    for (size_t num_els : sizes_vec) {
      std::vector<int> vec(num_els);
      for (size_t i = 0; i < num_els; ++i) {
        vec[i] = values_rng.GenerateValue();
      }
      // Compute sum - this part identical for two methods
      for (size_t i = 0; i < num_els; ++i) {
        method_1_sum += vec[i];
      }
    }
    double method_1_seconds = timer.GetSeconds();
    // Method 2: reserve then push_back
    timer.Reset();
    int method_2_sum = 0;
    for (size_t num_els : sizes_vec) {
      std::vector<int> vec;
      vec.reserve(num_els);
      for (size_t i = 0; i < num_els; ++i) {
        vec.push_back(values_rng.GenerateValue());
      }
      // Compute sum - this part identical for two methods
      for (size_t i = 0; i < num_els; ++i) {
        method_2_sum += vec[i];
      }
    }
    double method_2_seconds = timer.GetSeconds();
    // Report results as mean_size, method_1_seconds, method_2_seconds
    std::cout << mean_size << ", " << method_1_seconds << ", " << method_2_seconds;
    // Do something with the dummy sums that cannot be optimized out
    std::cout << ((method_1_sum > method_2_sum) ? "" : " ") << std::endl;
  }

  return 0;
}

Заголовочные файлы, которые я использовал, находятся здесь: