Подтвердить что ты не робот

Перемещение назначения медленнее, чем копирование - ошибка, функция или неуказанная?

Недавно я понял, что добавление семантики перемещения в С++ 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. Тем не менее, я не знаю, каковы правила аннулирования итератора для назначения переадресации, поэтому, если вы знаете о них, возможно, стоит упомянуть их в вашем ответе.

4b9b3361

Ответ 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) запутался.