Когда кеширование процессора становится лучше и лучше std::vector
обычно превосходит std::list
, даже когда дело доходит до тестирования сильных сторон std::list
. По этой причине даже в ситуациях, когда мне нужно удалить/вставить в середине контейнера, я обычно выбираю std::vector
, но я понял, что никогда не тестировал это, чтобы убедиться, что предположения верны. Поэтому я установил тестовый код:
#include <iostream>
#include <chrono>
#include <list>
#include <vector>
#include <random>
void TraversedDeletion()
{
std::random_device dv;
std::mt19937 mt{ dv() };
std::uniform_int_distribution<> dis(0, 100000000);
std::vector<int> vec;
for (int i = 0; i < 100000; ++i)
{
vec.emplace_back(dis(mt));
}
std::list<int> lis;
for (int i = 0; i < 100000; ++i)
{
lis.emplace_back(dis(mt));
}
{
std::cout << "Traversed deletion...\n";
std::cout << "Starting vector measurement...\n";
auto now = std::chrono::system_clock::now();
auto index = vec.size() / 2;
auto itr = vec.begin() + index;
for (int i = 0; i < 10000; ++i)
{
itr = vec.erase(itr);
}
std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
}
{
std::cout << "Starting list measurement...\n";
auto now = std::chrono::system_clock::now();
auto index = lis.size() / 2;
auto itr = lis.begin();
std::advance(itr, index);
for (int i = 0; i < 10000; ++i)
{
auto it = itr;
std::advance(itr, 1);
lis.erase(it);
}
std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
}
}
void RandomAccessDeletion()
{
std::random_device dv;
std::mt19937 mt{ dv() };
std::uniform_int_distribution<> dis(0, 100000000);
std::vector<int> vec;
for (int i = 0; i < 100000; ++i)
{
vec.emplace_back(dis(mt));
}
std::list<int> lis;
for (int i = 0; i < 100000; ++i)
{
lis.emplace_back(dis(mt));
}
std::cout << "Random access deletion...\n";
std::cout << "Starting vector measurement...\n";
std::uniform_int_distribution<> vect_dist(0, vec.size() - 10000);
auto now = std::chrono::system_clock::now();
for (int i = 0; i < 10000; ++i)
{
auto rand_index = vect_dist(mt);
auto itr = vec.begin();
std::advance(itr, rand_index);
vec.erase(itr);
}
std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
std::cout << "Starting list measurement...\n";
now = std::chrono::system_clock::now();
for (int i = 0; i < 10000; ++i)
{
auto rand_index = vect_dist(mt);
auto itr = lis.begin();
std::advance(itr, rand_index);
lis.erase(itr);
}
std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
}
int main()
{
RandomAccessDeletion();
TraversedDeletion();
std::cin.get();
}
Все результаты скомпилированы с помощью /02 (Maximize speed)
.
Первый, RandomAccessDeletion()
, генерирует случайный индекс и стирает этот индекс 10.000 раз. Мои предположения были правильными, и вектор действительно намного быстрее, чем список:
Удаление случайного доступа...
Начало измерения вектора...
Взял 240299 мкс
Измерение начального списка...
Взял 1368205 мкс
Вектор на 5,6 раза быстрее, чем в списке. Мы можем, скорее всего, поблагодарить наших кэшистов за это преимущество в производительности, хотя нам нужно сдвинуть элементы в векторе при каждом удалении, которое оно влияет, меньше времени поиска списка, как мы можем видеть в тесте.
Итак, я добавил еще один тест, увиденный в TraversedDeletion()
. Он не использует рандомизированные позиции для удаления, а скорее выбирает индекс в середине контейнера и использует его как базовый итератор, а затем пересекает контейнер, чтобы стереть 10.000 раз.
Мои предположения состояли в том, что список будет превосходить вектор лишь незначительно или быстро, как вектор.
Результаты для одного и того же выполнения:
Отслеживание удаления...
Начало измерения вектора....
Взял 195477 мкс
Измерение начального списка...
Взял 581 мкс
Wow. Список содержит 336x быстрее. Это действительно далеко от моих ожиданий. Таким образом, наличие нескольких промахов в списке не похоже вообще на то, что сокращение времени поиска для списка весит больше.
Итак, список, по-видимому, по-прежнему имеет очень сильную позицию, когда дело доходит до производительности для угловых/необычных случаев, или мои тестовые примеры некорректны?
Означает ли это, что список в настоящее время является лишь разумным вариантом для большого количества вставок/исключений в середине контейнера при прохождении или есть другие случаи?
Есть ли способ изменить векторный доступ и стирание в TraversedDeletion()
, чтобы сделать его по меньшей мере более конкурентным по сравнению с списком?
В ответ на комментарий @BoPersson:
vec.erase(it, it+10000)
будет намного лучше, чем 10000 отдельные удаления.
Изменение:
for (int i = 0; i < 10000; ++i)
{
itr = vec.erase(itr);
}
To:
vec.erase(itr, itr + 10000);
Дал мне:
Начало измерения вектора...
Взял 19 мкс
Это уже значительное улучшение.