Перемещение назначения медленнее, чем копирование - ошибка, функция или неуказанная?
Недавно я понял, что добавление семантики перемещения в С++ 11 (или, по крайней мере, моя реализация этого, Visual С++) активно (и довольно резко) нарушило одну из моих оптимизаций.
Рассмотрим следующий код:
#include <vector>
int main()
{
typedef std::vector<std::vector<int> > LookupTable;
LookupTable values(100); // make a new table
values[0].push_back(1); // populate some entries
// Now clear the table but keep its buffers allocated for later use
values = LookupTable(values.size());
return values[0].capacity();
}
Я следил за этим типом шаблона для повторного использования контейнеров: я бы повторно использовал один и тот же контейнер вместо того, чтобы уничтожать и воссоздавать его, чтобы избежать ненужного перераспределения кучи и (немедленного) перераспределения.
В С++ 03 это сработало нормально - это означает, что этот код использовался для возврата 1
, потому что векторы были скопированы по-разному, а их базовые буферы были сохранены как есть. Следовательно, я мог бы модифицировать каждый внутренний вектор, зная, что он может использовать тот же буфер, что и раньше.
В С++ 11, однако, я заметил, что это приводит к перемещению правой части в левую сторону, которая выполняет элементное перемещение-присваивание каждому вектору с левой стороны, Это, в свою очередь, приводит к тому, что вектор отбрасывает свой старый буфер, внезапно уменьшая его емкость до нуля. Следовательно, мое приложение теперь значительно замедляется из-за избыточного распределения/освобождения кучи.
Мой вопрос: это поведение является ошибкой, или это намеренно? Он даже определен стандартом вообще?
Обновление:
Я только понял, что правильность этого конкретного поведения может зависеть от того, может ли a = A()
аннулировать итераторы, указывающие на элементы a
. Тем не менее, я не знаю, каковы правила аннулирования итератора для назначения переадресации, поэтому, если вы знаете о них, возможно, стоит упомянуть их в вашем ответе.
Ответы
Ответ 1
С++ 11
Разница в поведении в OP между С++ 03 и С++ 11 обусловлена тем, как реализовано назначение перемещения.
Существует два основных варианта:
-
Уничтожьте все элементы LHS. Освободите основное хранилище LHS. Переместите базовый буфер (указатели) из RHS в LHS.
-
Переместить-присвойте элементы RHS элементам LHS. Уничтожьте все лишние элементы LHS или переместите новые элементы в LHS, если у RHS больше.
Я думаю, что можно использовать опцию 2 с копиями, если перемещение не исключено.
Вариант 1 отменяет все ссылки/указатели/итераторы на LHS и сохраняет все итераторы и т.д. RHS. Он нуждается в разрушениях O(LHS.size())
, но само движение буфера - O (1).
Вариант 2 делает недействительными только итераторы для избыточных элементов LHS, которые уничтожаются, или всех итераторов, если происходит перераспределение LHS. Это O(LHS.size() + RHS.size())
, так как все элементы обеих сторон должны быть позабочены (скопированы или уничтожены).
Насколько я могу судить, нет гарантии того, что происходит в С++ 11 (см. следующий раздел).
В теории вы можете использовать параметр 1, когда вы можете освободить базовый буфер с помощью распределителя, который хранится в LHS после операции. Это может быть достигнуто двумя способами:
-
Если два распределителя сравниваются равными, можно использовать для освобождения хранилища, выделенного с помощью другого. Поэтому, если распределители LHS и RHS сравниваются равными перед перемещением, вы можете использовать опцию 1. Это решение во время выполнения.
-
Если распределитель может распространяться (перемещаться или копироваться) с RHS на LHS, этот новый распределитель в LHS может использоваться для освобождения хранилища RHS. Независимо от того, распространяется ли распределитель, определяется allocator_traits<your_allocator :: propagate_on_container_move_assignment
. Это определяется свойствами типа, то есть решением времени компиляции.
С++ 11 минус дефекты/С++ 1y
После LWG 2321 (который все еще открыт), у нас есть гарантия, что:
нет конструктора перемещения (или переместить оператор присваивания, когда allocator_traits<allocator_type> :: propagate_on_container_move_assignment :: value
is true
) контейнера (кроме массива) делает недействительными любые ссылки, указатели или итераторы, ссылающиеся на элементы источника контейнер. [Примечание: Итератор end()
не ссылается ни на какой элемент, поэтому он может быть признан недействительным. - конечная нота]
Это требует, чтобы присваивание перемещения для этих распределителей, которые распространяются при назначении перемещения, должно перемещать указатели объекта vector
, но не должно перемещать элементы вектора. (вариант 1)
Распределитель по умолчанию, после LWG defect 2103, распространяется во время перемещения-назначения контейнера, поэтому трюк в OP запрещен переместите отдельные элементы.
Мой вопрос: это поведение является ошибкой, или это намеренно? Он даже определен стандартом вообще?
Нет, да, нет (возможно).
Ответ 2
См. этот ответ для подробного описания того, как должно выполняться переадресация vector
. Когда вы используете std::allocator
, С++ 11 помещает вас в случай 2, который многие из членов комитета считают дефектом, и был исправлен в случае 1 для С++ 14.
Оба случая 1 и случай 2 имеют одинаковое поведение во время выполнения, но в случае 2 есть дополнительные требования времени компиляции на vector::value_type
. Как в случае 1, так и в случае 2 приводят к тому, что владение памятью передается от rhs к lhs во время назначения перемещения, что дает вам результаты, которые вы наблюдаете.
Это не ошибка. Это намеренно. Он задается С++ 11 и forward. Да, есть некоторые незначительные недостатки, о которых указал dyp в своем ответе. Но ни один из этих дефектов не изменит поведение, которое вы видите.
Как было отмечено в комментариях, самым простым решением для вас является создание помощника as_lvalue
и использование этого:
template <class T>
constexpr
inline
T const&
as_lvalue(T&& t)
{
return t;
}
//...
// Now clear the table but keep its buffers allocated for later use
values = as_lvalue(LookupTable(values.size()));
Это нулевая стоимость и возвращает вам точно поведение С++ 03. Возможно, он не прошел проверку кода. Было бы понятнее, чтобы вы проследовали через и clear
каждый элемент внешнего вектора:
// Now clear the table but keep its buffers allocated for later use
for (auto& v : values)
v.clear();
Последнее я рекомендую. Первый из них (imho) запутался.