C Почему вставка неинициализированного вектора выполняется так же быстро, как push_back и присваивание?

#c #performance #vector #c 17

#c #Производительность #вектор #c 17

Вопрос:

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

  1. vector_using_assignment.cc : Инициализируйте вектор до 1 000 000 и назначьте его элементы в цикле for
 // 1. vector_using_assignment
#include <iostream>
#include <vector>

int main(int argc, char *argv[]) {
  int N = *argv[1];
  std::vector<int> V(N);
  for (int i =0; i < N; i  ) {
      V[i] = i;
  }  
}
$ g   -O0 -std=c  17 vector_using_assignment.cc -o vector_using_assignment
$ time ./vector_using_assignment 1000000
real    0m0.004s
user    0m0.003s
sys     0m0.001s

 
  1. vector_using_push_back.cc : Создайте пустой вектор, а затем назначьте его элементы в цикле for, используя метод push_back
 // 2. vector_using_push_back.cc
#include <iostream>
#include <vector>
int main(int argc, char *argv[]) {
  int N = *argv[1];
  std::vector<int> V;
  for (int i =0; i < N; i  ) {
      V.push_back(i);
  }    
}
$ g   -O0 -std=c  17 vector_using_push_back.cc -o vector_using_push_back
$ time ./vector_using_push_back 1000000
real    0m0.004s
user    0m0.004s
sys     0m0.000s

 
  1. vector_using_insert.cc Создайте пустой вектор, а затем назначьте его элементы в цикле for, используя метод insert
 // 3. vector_using_insert.cc
#include <iostream>
#include <vector>
int main(int argc, char *argv[]) {
  int N = *argv[1];
  std::vector<int> V;
  for (int i =0; i < N; i  ) {
  V.insert(V.begin(), N - i - 1);
  }
}
$ g   -O0 -std=c  17 vector_using_insert.cc -o vector_using_insert
$ time ./vector_using_insert 1000000
  real  0m0.004s
  user  0m0.003s
  sys   0m0.001s
 

Как вы можете видеть, все методы в точности равны. Я понимаю, что push_back это линейно по сложности (когда размер <емкость). В этом примере это явно не так. Разве insert метод не должен быть линейным?

Я предполагаю, что:

  1. c 17 выполняется ли некоторая оптимизация, хотя я отключил оптимизацию?
  2. На моей машине 2 процессора, я думаю, по 20 ядер каждый, и 32 ГБ оперативной памяти, так что это заставляет вести себя иначе, чем я думаю? Я попробовал 100 000 000, но по-прежнему не видел никаких изменений

Что я здесь делаю не так?

Комментарии:

1. Он все равно может оптимизировать некоторые, например, удалить код, который не имеет никакого заметного эффекта, что означает, что он может оптимизировать все в ваших программах. Кроме того, ваш N цикл будет слишком мал, чтобы его можно было заметить даже на современном компьютере.

2. int N = *argv[1]; странно. Обычно вы хотели бы преобразовать строку в int . Я бы предположил N , что это совсем не то, что вы могли подумать. Распечатайте его. Вероятно, это 49.

3. Просто случайно, вы измеряете слишком маленький интервал времени. Было бы более целесообразно запускать тест много раз, чтобы усилить любые различия во времени. Теоретически, ваш тест # 1 имеет O (2N) обхода памяти и одно выделение. В двух других случаях потенциально O (logN) распределений амортизируется до O (N) копий, что равно O (N) обходу. Могут возникнуть проблемы с кэшем, и, как предполагает Ted, возможно, ваш код оптимизирован, потому что вы ничего не делали с данными. Рассмотрим шаг после синхронизации, который усредняет содержимое вектора в volatile.

4. @RetiredNinja, ты угадал. N равно 49.

5. Re: «Я понимаю, что push_back линейен по сложности (когда size < capacity)» — на самом деле push_back на удивление всегда постоянное время. Прочитайте о амортизированной временной сложности std::vector::push_back . В insert таких случаях метод также может быть постоянным, но только при вставке в конец вектора. В общем, вставка в конце похожа на push_back . Вставка в любом другом месте является линейной, и в вашем случае цикл для вставки в начале является полиномиальным.

Ответ №1:

Как и ожидалось, была ошибка в том, как я преобразовал аргумент stdin в int. Правильный способ:

 int N = atoi(argv[1]);
std::cout << "N = " << N << std::endl;
 

Теперь я вижу некоторые реальные различия:

 $ time ./vector_using_push_back 1000000000
N = 1000000000

real    0m20.139s
user    0m16.437s
sys 0m3.701s

$ time ./vector_using_assignment 1000000000
N = 1000000000

real    0m7.530s
user    0m6.005s
sys 0m1.525s

$ time ./vector_using_insert 1000000000
N = 1000000000
# I gave up after a couple of min

 

Комментарии:

1. Что касается вашего другого вопроса о том, что метод вставки является линейным: обратите внимание, что вы вставляете в начале, что не является линейной операцией std::vector . Это объясняет мучительную производительность (потому что ваш тест равен O (N ^ 2)). Для развлечения я собрал аналогичную программу (но выполнил линейную операцию для вставки) здесь: godbolt.org/z/naM35MPPf — Я также добавил четвертый тест, используя рекомендуемый подход резервирования хранилища перед отправкой известного количества данных. Производительность этого более близка к наивной инициализации.

2. atoi возвращает int ; нет необходимости приводить результат.