Почему copy_if работает медленнее, чем copy
В настоящее время я работаю над своим графическим движком OpenGL. И я пытался придумать лучший способ передать большое количество объектов в GPU для создания инстансов отрисовки. Самая большая проблема для меня в том, что некоторые объекты могут быть мертвыми, поэтому я создал небольшой тест.
Вот простая структура, которую я тестировал (в реальном приложении это будет позиция + цвета и т. д.)
struct foo
{
bool is_active = false;
float value = 0.0f;
};
После этого я создал эти контейнеры:
// All data
std::vector<foo> data_vector;
// Data that is only active
std::vector<foo> active_vector;
using distance_t = vector<foo>::iterator::difference_type;
// List of segments, so that if we have 10 elements where
// only the 5th is not active it is going to look like that
// { {0,5}, {6, 10} }
std::list<pair<distance_t, distance_t>> active_segments;
Зарезервированное пространство для 1 000 000 элементов в векторах. Залил data_vector всеми истинными значениями. Список также заполнили, чтобы не учитывать время выделения. И проверил скорость этих 3 функций копирования с помощью high_resolution_clock
// First method
// For all true values *active_segments* has only one element with
// {0, 1000000}
for_each(active_segments.begin(), active_segments.end(),
[&active_vector, data_vector](auto current)
{
copy(data_vector.begin() + current.first,
data_vector.begin() + current.second,
std::back_inserter(active_vector));
});
// Second method
copy_if (data_vector.begin(), data_vector.end(),
std::back_inserter(active_vector),
[](const foo ¤t)
{
return current.is_active;
});
// Third method
copy(data_vector.begin(), data_vector.end(),
std::back_inserter(active_vector));
Очевидно, копировать был самым быстрым с 18024 микросекундами. Но меня удивило, что copy_if быстрее (27777 микросекунд), чем первый метод (33278 микросекунд).
Я не понимаю, почему это происходит. Я хотел выделить дополнительную память, но увеличить скорость копирования, но в результате у меня есть метод, который работает медленнее даже в лучших условиях.
Что произойдет, если вы используете std :: vector вместо std :: list для active_segments?
Попробуйте active_vector.insert(active_vector.end(), data_vector.begin() + current.first, data_vector.begin() + current.second). Это должно быть еще быстрее, поскольку позволяет избежать вызова push_back для каждого элемента.





Мне кажется, что у вас есть комбинация (по крайней мере) двух факторов, ведущих к проблеме.
Первая представляет собой настоящую проблему: в вашей лямбде вы захватываете data_vector по значению, а не по ссылке, поэтому вы копируете весь входной массив, а затем копируете данные из этой копии в результат.
Второй в основном относится к тестированию: разогрев кеша. Если я исправлю лямбду так, чтобы она захватывалась по ссылке, ваш метод 1 по-прежнему будет работать значительно медленнее, чем два других метода. Но, если я добавлю перед ним простой цикл подогрева кеша:
for (int i = 0; i < size; i++)
active_vector.push_back(data_vector[i]);
... затем я могу бежать за всеми тремя после этого, и все они бегут достаточно близко к той же скорости, что я больше не могу быть уверен, что один быстрее другого.
С другой стороны, я считаю, что это также указывает на бессмысленность всего упражнения - хотя copy_if теоретически должен быть немного медленнее, чем copy (по каждому элементу), я не могу найти какой-либо существенной разницы между два. Я подозреваю, что в большинстве случаев пропускная способность памяти является ограничивающим фактором, а дополнительное время обработки, чтобы выяснить, нужно ли что-то копировать, просто теряется в шуме. Фактически, иногда вторая версия (с использованием copy_if) выходит самой быстрой, а третья (с использованием copy) - самой медленной:
method 1: 3,295us
method 2: 3,178us
method 3: 3,839us
Просто для чего это стоит, вот код, который я запускал:
#include <algorithm>
#include <chrono>
#include <iostream>
#include <vector>
#include <list>
#include <utility>
struct foo
{
bool is_active = true;
float value = 0.0f;
};
int main() {
const int size = 1'000'000;
std::cout.imbue(std::locale(""));
// All data
std::vector<foo> data_vector(size);
// Data that is only active
std::vector<foo> active_vector;
using distance_t = std::vector<foo>::iterator::difference_type;
// List of segments, so that if we have 10 elements where
// only the 5th is not active it is going to look like that
// { {0,5}, {6, 10} }
std::vector<std::pair<distance_t, distance_t>> active_segments;
using namespace std::chrono;
// Warm the cache:
for (int i = 0; i < size; i++)
active_vector.push_back(data_vector[i]);
{
active_segments.emplace_back(0, size);
active_vector.clear();
active_vector.reserve(size);
auto begin = high_resolution_clock::now();
for_each(active_segments.begin(), active_segments.end(),
[&active_vector, &data_vector](auto current)
{
copy(data_vector.begin() + current.first,
data_vector.begin() + current.second,
std::back_inserter(active_vector));
});
auto end = high_resolution_clock::now();
std::cout << "method 1: " << duration_cast<microseconds>(end - begin).count() << "us\n";
}
{
active_vector.clear();
active_vector.reserve(size);
auto begin = high_resolution_clock::now();
// Second method
copy_if (data_vector.begin(), data_vector.end(),
std::back_inserter(active_vector),
[](const foo ¤t)
{
return current.is_active;
});
auto end = high_resolution_clock::now();
std::cout << "method 2: " << duration_cast<microseconds>(end - begin).count() << "us\n";
}
{
active_vector.clear();
active_vector.reserve(size);
auto begin = high_resolution_clock::now();
copy(data_vector.begin(), data_vector.end(),
std::back_inserter(active_vector));
auto end = high_resolution_clock::now();
std::cout << "method 3: " << duration_cast<microseconds>(end - begin).count() << "us\n";
}
}
Есть еще один момент, который, вероятно, следует учитывать: вам все еще нужны элементы в data_vector, которые больше не активны? Если они вам больше не нужны, вы можете использовать std::remove_if, чтобы переместить все активные элементы в начало коллекции, а затем стереть оттуда до конца.
auto e = std::remove_if (data_vector.begin(), data_vector.end(),
[](auto const &e) {return e.is_active; });
data_vector.erase(e, data_vector.end());
Быстрый тест с 50% вероятностью того, что каждый элемент будет помечен как активный или неактивный, показывает, что он работает примерно в два раза быстрее, чем копирование активных элементов.
Большое спасибо за ответ. Я очень пропустил ссылку. Теперь у меня лучшие результаты с первым методом в массиве со всеми истинными значениями. Но со случайно назначенными логическими значениями дело обстоит иначе. Попробую проверить прогрев кеша. И мне не хватало того, что я использовал 10 миллионов вместо 1 миллиона, поэтому мое время было неправильным, но идея та же.
вам также следует захватить
data_vectorпо ссылке.