Push_back для вектора, deque и списков

Я пытаюсь оптимизировать подпрограмму С++. Основным узким местом в этой подпрограмме является push_back() вектора объектов. Вместо этого я попытался использовать deque и даже попробовал список. Но странно (и вопреки теории) реализация deque и list выполняется намного медленнее, чем векторная копия.

Фактически даже clear() работает намного медленнее для реализации deque и list, чем векторный аналог. В этом случае реализация Vector, по-видимому, является самой быстрой, тогда как реализация списка является самой медленной.

Любые указатели?

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

Спасибо.

Ответ 1

вектор, который быстрее создается или очищается, чем ожидалось; это более простая структура данных.

Что касается vector::push_back, он должен сделать две вещи:

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

Вы можете в целом ускорить процесс, исключив шаг 1, просто изменив размер вектора и используя operator[] для установки элементов.

UPDATE: Оригинальный плакат попросил пример. Код ниже 128 мега вставки и выходы

push_back           : 2.04s
reserve & push_back : 1.73s
resize & place      : 0.48s

при компиляции и запуске с g++ -O3 на Debian/Lenny на старой машине P4.

#include <iostream>
#include <time.h>
#include <vector>

int main(int,char**)
{
  const size_t n=(128<<20);

  const clock_t t0=clock();
  {
    std::vector<unsigned char> a;
    for (size_t i=0;i<n;i++) a.push_back(i);
  }
  const clock_t t1=clock();
  {
    std::vector<unsigned char> a;
    a.reserve(n);
    for (size_t i=0;i<n;i++) a.push_back(i);
  }
  const clock_t t2=clock();
  {
    std::vector<unsigned char> a;
    a.resize(n);
    for (size_t i=0;i<n;i++) a[i]=i;
  }
  const clock_t t3=clock();

  std::cout << "push_back           : " << (t1-t0)/static_cast<float>(CLOCKS_PER_SEC) << "s" << std::endl;
  std::cout << "reserve & push_back : " << (t2-t1)/static_cast<float>(CLOCKS_PER_SEC) << "s" << std::endl;
  std::cout << "resize & place      : " << (t3-t2)/static_cast<float>(CLOCKS_PER_SEC) << "s" << std::endl;

  return 0;  
}

Ответ 2

Если вы не знаете, сколько объектов вы будете добавлять, очень сложно придумать оптимальное решение. Все, что вы можете сделать, это попытаться минимизировать затраты, которые, как вы знаете, происходят, что в данном случае означает, что ваш вектор постоянно изменяется.

Вы можете сделать это двумя способами:

1) Разделите свою операцию на сборку и завершение. Здесь вы создаете список в вектор, который гарантированно будет достаточно большим, а когда он будет скопирован в другой вектор.

например.

std::vector<Foo> hugeVec;
hugeVec.reserve(1000);    // enough for 1000 foo's

// add stuff

std::vector<Foo> finalVec;
finalVec = hugeVec;

2) Альтернативно, когда ваш вектор является полным резервом вызова с достаточным для другого набора объектов,

if (vec.capacity() == vec.size())
  vec.reserve(vec.size() + 16);  // alloc space for 16 more objects

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

Ответ 3

Вы отбрасываете сами объекты или указатель на них? Указатели обычно будут намного быстрее, поскольку для копирования нужно всего 4-8 байт, по сравнению с любым размером объектов.

Ответ 4

"push_back()" может быть медленным, если копия объекта работает медленно. Если конструктор по умолчанию работает быстро, и у вас есть способ использовать swap, чтобы избежать копирования, вы могли бы иметь гораздо более быструю программу.

void test_vector1()
{
    vector<vector<int> > vvi;
    for(size_t i=0; i<100; i++)
    {
        vector<int> vi(100000, 5);
        vvi.push_back(vi);    // copy of a large object
    }
}

void test_vector2()
{
    vector<int> vi0;
    vector<vector<int> > vvi;
    for(size_t i=0; i<100; i++)
    {
        vector<int> vi(100000, 5);
        vvi.push_back(vi0);  // copy of a small object
        vvi.back().swap(vi); // swap is fast
    }
}

Результаты:

VS2005-debug 
* test_vector1 -> 297
* test_vector2 -> 172

VS2005-release
* test_vector1 -> 203
* test_vector2 -> 94

gcc
* test_vector1 -> 343
* test_vector2 -> 188

gcc -O2
* test_vector1 -> 250
* test_vector2 -> 156

Ответ 5

Если вы хотите, чтобы вектор был быстрым, вы должны зарезервировать() достаточно места. Это имеет огромное значение, потому что каждый рост ужасно дорог. Если вы не знаете, сделайте хорошее предположение.

Ответ 6

Вам нужно будет предоставить дополнительную информацию о поведении рутины.

В одном месте вас беспокоит скорость push_back() в другой, вас беспокоит clear(). Собираете ли вы контейнер, что-то делаете, а затем демпируете его?

Результаты, которые вы видите для clear(), связаны с тем, что vector<> должен только освободить единый блок памяти, deque<> должен освободить несколько, а list<> должен отпустить один для каждого элемента.

Ответ 7

Deque имеет более сложную структуру, чем вектор, и различия в скорости между ними будут в значительной степени зависеть как от конкретной реализации, так и от фактического количества отброшенных элементов, но для больших объемов данных она должна быть быстрее. clear() может быть медленнее, потому что он может избавиться от более сложных базовых структур. То же самое касается списка.

Ответ 8

Что касается push_back(), выполняющегося медленно, и резерв не помогает, реализация STL, используемого в MSVC, работает примерно так: когда вы сначала создаете вектор, он резервирует пространство для 10 элементов. С этого момента, когда он заполняется, он оставляет пространство в 1,5 раза больше количества элементов в векторе. Итак, что-то вроде 10, 15, 22, 33, 49, 73, 105, 157... Перераспределения стоят дорого.

Даже если вы не знаете точный размер, резерв() может быть полезен. reserve() не препятствует росту вектора, если это необходимо. Если вы резервируете(), и вектор растет выше этого размера, вы все равно улучшаете ситуацию из-за резерва. Если вектор окажется намного меньше, ну, может быть, это нормально, потому что производительность в целом работает лучше с меньшими размерами.

Вам нужно создать профиль в режиме RELEASE, чтобы точно знать, какая стратегия работает лучше всего.

Ответ 9

Вы должны выбрать свой контейнер в соответствии с тем, что вы собираетесь с ним делать.

Релевантными действиями являются: расширение (с push), вставка (может вообще не понадобиться), извлечение, удаление.

В cplusplus.com, есть очень хороший обзор операций для типа контейнера.

Если операция push -bound, имеет смысл, что вектор бьет все остальные. Хорошая вещь о deque заключается в том, что он выделяет фиксированные куски, поэтому будет более эффективно использовать фрагментированную память.