Идеальная пересылка с несколькими проходами по входным аргументам
Рассмотрим следующую функцию accept
который принимает "универсальную ссылку" типа T
и направляет это к parse<T>()
Функциональный объект с перегрузкой для lvalues и один для rvalues:
template<class T>
void accept(T&& arg)
{
parse<T>()(std::forward<T>(arg), 0); // copy or move, depending on rvaluedness of arg
}
template<class T>
class parse
{
// parse will modify a local copy or move of its input parameter
void operator()(T const& arg, int n) const { /* optimized for lvalues */ }
void operator()(T&& arg) , int n) const { /* optimized for rvalues */ }
};
Так как совершенная пересылка оставляет исходный объект в допустимом, но неопределенном состоянии, невозможно совершенную пересылку снова в той же области видимости. Ниже моя попытка иметь как можно меньше копий в гипотетическом split()
функция, которая принимает int
представляющий число проходов, которые должны быть сделаны над входными данными:
template<class T>
void split(T&& arg, int n)
{
for (auto i = 0; i < n - 1; ++i)
parse<T>()(arg , i); // copy n-1 times
parse<T>()(std::forward<T>(arg), n - 1); // possibly move the n-th time
}
Вопрос: это рекомендуемый способ применения идеальной переадресации для нескольких проходов по одним и тем же данным? Если нет, то какой более идиоматичный способ минимизировать количество копий?
1 ответ
Вопрос: это рекомендуемый способ применения идеальной переадресации для нескольких проходов по одним и тем же данным?
Да, это рекомендуемый способ применения совершенной переадресации (или перемещения), когда вам необходимо передать данные несколько раз. Только (потенциально) переходить от него при последнем доступе. Действительно, этот сценарий был предусмотрен в первоначальной статье о перемещении, и это та самая причина, по которой "именованные" переменные, объявленные с типом rvalue-reference, не переносятся неявно. От N1377:
Несмотря на то, что именованные ссылки rvalue могут связываться с rvalue, при использовании они обрабатываются как lvalue. Например:
struct A {};
void h(const A&);
void h(A&&);
void g(const A&);
void g(A&&);
void f(A&& a)
{
g(a); // calls g(const A&)
h(a); // calls h(const A&)
}
Хотя r-значение может связываться с параметром "a" функции f(), после привязки a теперь рассматривается как l-значение. В частности, вызовы перегруженных функций g() и h() преобразуются в постоянные перегрузки A& (lvalue). Обработка "a" как значения r в пределах f приведет к появлению кода, подверженного ошибкам: сначала будет вызвана "версия перемещения" функции g(), которая, скорее всего, будет воровать "a", а затем ворованное "a" будет отправлено переместить перегрузку h().
Если ты хочешь h(a)
чтобы перейти в приведенном выше примере, вы должны сделать это явно:
h(std::move(a)); // calls h(A&&);
Как указывает Casey в комментариях, у вас есть проблема с перегрузкой при передаче lvalues:
#include <utility>
#include <type_traits>
template<class T>
class parse
{
static_assert(!std::is_lvalue_reference<T>::value,
"parse: T can not be an lvalue-reference type");
public:
// parse will modify a local copy or move of its input parameter
void operator()(T const& arg, int n) const { /* optimized for lvalues */ }
void operator()(T&& arg , int n) const { /* optimized for rvalues */ }
};
template<class T>
void split(T&& arg, int n)
{
typedef typename std::decay<T>::type Td;
for (auto i = 0; i < n - 1; ++i)
parse<Td>()(arg , i); // copy n-1 times
parse<Td>()(std::forward<T>(arg), n - 1); // possibly move the n-th time
}
Выше я исправил это, как предлагает Кейси, создавая parse<T>
только на не ссылочных типах, использующих std::decay
, Я также добавил static_assert, чтобы клиент случайно не допустил эту ошибку. static_assert
не является строго необходимым, потому что вы получите ошибку во время компиляции. Тем не менее static_assert
может предложить более читаемое сообщение об ошибке.
Это не единственный способ решить проблему. Другой способ, который позволил бы клиенту создать экземпляр parse
с ссылочным типом lvalue, для частичной специализации разбора:
template<class T>
class parse<T&>
{
public:
// parse will modify a local copy or move of its input parameter
void operator()(T const& arg, int n) const { /* optimized for lvalues */ }
};
Теперь клиенту не нужно делать decay
танец:
template<class T>
void split(T&& arg, int n)
{
for (auto i = 0; i < n - 1; ++i)
parse<T>()(arg , i); // copy n-1 times
parse<T>()(std::forward<T>(arg), n - 1); // possibly move the n-th time
}
И вы можете применить специальную логику под parse<T&>
если необходимо.
(Я знаю, это старая ветка)
Как указано в комментариях, данные представляют собой большой массив или вектор uint64_t. Лучшей оптимизацией, чем передача параметров, для предотвращения окончательного копирования, вероятно, будет оптимизация многих операций копирования для
- прочитать один раз
- писать много раз (для каждого предполагаемого прохода)
за один шаг вместо множества независимых копий.
Отправной точкой может быть эта более быстрая альтернатива memcpy? который имеет ответы, которые включают в себя код, подобный memcpy. Вам придется умножить строку кода, которая записывает в место назначения, чтобы вместо этого записать несколько копий данных.
Вы также можете комбинировать memset, который оптимизирован для многократной записи одного и того же значения в память, и memcpy, который оптимизирован для чтения и записи блоков памяти один раз для каждого блока. Вы можете ознакомиться с оптимизированным исходным кодом здесь: https://github.com/KNNSpeed/AVX-Memmove
Лучший код будет зависеть от используемой архитектуры и процессора. Так что вам придется протестировать и сравнить достигнутую скорость.