Copy_if против сохранения последовательностей и использования копирования

Почему 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 &current)
        {
           return current.is_active;
        });

// Third method
copy(data_vector.begin(), data_vector.end(), 
     std::back_inserter(active_vector));

Очевидно, копировать был самым быстрым с 18024 микросекундами. Но меня удивило, что copy_if быстрее (27777 микросекунд), чем первый метод (33278 микросекунд).

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

вам также следует захватить data_vector по ссылке.

apple apple 07.08.2018 15:13

Что произойдет, если вы используете std :: vector вместо std :: list для active_segments?

BDL 07.08.2018 15:13

Попробуйте active_vector.insert(active_vector.end(), data_vector.begin() + current.first, data_vector.begin() + current.second). Это должно быть еще быстрее, поскольку позволяет избежать вызова push_back для каждого элемента.

Igor Tandetnik 07.08.2018 15:32
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
152
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мне кажется, что у вас есть комбинация (по крайней мере) двух факторов, ведущих к проблеме.

Первая представляет собой настоящую проблему: в вашей лямбде вы захватываете 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 &current)
        {
            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 миллиона, поэтому мое время было неправильным, но идея та же.

VioletOil 07.08.2018 18:49

Другие вопросы по теме