Как семантика перемещения применяется к следующему фрагменту, если xvalue отсутствует? - PullRequest
1 голос
/ 04 февраля 2020

Я наткнулся на следующую статью и не понимаю разницу в производительности между C ++ 98 и C ++ 11, которая, как говорит автор, приписывается семантике перемещения.

#include <vector>

using namespace std;

int main() {
    vector<vector<int> > V;

    for(int k = 0; k < 100000; ++k) {
        vector<int> x(1000);
        V.push_back(x);
    }

    return 0;
}

Насколько я знаю, V.push_back(x) не вызывает никакой семантики перемещения. Я считаю, что x является lvalue, и этот фрагмент вызывает один и тот же vector::push_back(const T&) в C ++ 98 и C ++ 11.

Код компилируется одинаково для любой версии: https://godbolt.org/z/q3Lzae

Является ли автор неверным в своем утверждении или компилятор достаточно умен, чтобы понять, что x собирается быть уничтоженным?

Если автор неверен, есть ли что-нибудь еще, присутствующее в C ++ 11, которое дало бы этому прирост производительности "без изменения строки кода"?

Ответы [ 2 ]

4 голосов
/ 04 февраля 2020

Вы правы, что объект x не будет перемещен из. Повышение производительности операций перемещения связано с другими k векторами, уже находящимися в V.

. По мере роста вектора (если reserve не использовался с достаточным размером), иногда потребуется перераспределить чтобы получить больший кусок памяти, так как его элементы должны быть в непрерывной памяти. Это не случается на каждом push_back, но в этом примере это иногда случается. Допустим, push_back и другие функции используют некоторую приватную функцию grow_capacity, которая получает достаточно памяти и затем создает объекты уже в векторе в этой памяти.

В C ++ 03 единственная Разумным способом создания объектов в новой памяти для произвольного параметра шаблона T является использование конструктора копирования T.

// C++03 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
    T* new_data = get_allocator().allocate(new_capacity);
    T* new_end = new_data;
    try {
        for (const_iterator iter = begin(); iter != end(); ++iter) {
            ::new(static_cast<void*>(new_end)) T(*iter); // T copy ctor!
            ++new_end;
        }
    } catch (...) {
        while (new_end != new_data) (--new_end)->~T();
        get_allocator().deallocate(new_data, new_capacity);
        throw;
    }

    // Clean up old objects and memory.
    for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
        riter->~T();
    get_allocator().deallocate(_data, _capacity);

    // Assign private members.
    _data = new_data;
    _capacity = new_capacity;
}

В C ++ 11 и более поздних версиях, когда std::vector<T> необходимо перераспределить на большую емкость, ему разрешено перемещать элементы T вместо их копирования, если это можно сделать без нарушения гарантии строгого исключения. Для этого требуется, чтобы конструктор перемещения был объявлен не генерирующим никаких исключений. Но если конструктор перемещения может сгенерировать, элементы должны быть скопированы старым способом, чтобы вектор оставался в согласованном состоянии, если это произойдет.

// C++17 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
    T* new_data = get_allocator().allocate(new_capacity);

    if constexpr (::std::is_nothrow_move_constructible_v<T>) {
        ::std::uninitialized_move(begin(), end(), new_data);   // T move ctor!
    } else {
        T* new_end = new_data;
        try {
            for (const T& old_obj : *this) {
                ::new(static_cast<void*>(new_end)) T(old_obj); // T copy ctor!
                ++new_end;
            }
        } catch (...) {
            while (new_end != new_data) (--new_end)->~T();
            get_allocator().deallocate(new_data, new_capacity);
            throw;
        }
    }

    for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
        riter->~T();
    get_allocator().deallocate(_data, _capacity);

    // Assign private members.
    _data = new_data;
    _capacity = new_capacity;
}

Так в контейнере с типом std::vector<std::vector<int> >, T - это std::vector<int>. Увеличение емкости C ++ 03 иногда требует большого числа конструкторов копирования, а затем деструкторов для std::vector<int>. Каждый конструктор копирования выделяет некоторую память и копирует 1000 int значений, а каждый деструктор освобождает часть памяти, так что это действительно сложится. Но в C ++ 11 std::vector, поскольку тип элемента std::vector<int> имеет конструктор перемещения noexcept, контейнер std::vector<std::vector<int>> может просто использовать этот конструктор перемещения, который представляет собой всего лишь несколько замен скалярных членов, а также заставляет деструкторы перемещенных старых объектов ничего не делать.

0 голосов
/ 04 февраля 2020

В этом примере происходит то, что x находится на грани go вне области действия в вызове push_back (это конец его времени жизни и последующего использования нет), поэтому компилятор может рассматривайте это как значение x и выходите из него. Это не один из случаев, когда компилятору требуется выполнить оптимизацию перемещения, так что это может и не произойти, но любой приличный компилятор сделает это, если оптимизация включена (и g cc, и clang будут использовать перемещение здесь).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...