Неэффективность идиомы копирования и свопинга?

Я тестировал некоторый код, в котором есть элемент данных std::vector внутри класса. Класс является одновременно подвижным и подвижным, а operator= реализуется, как описано здесь, используя идиома и .

Если есть два vector s, скажем v1 с большой емкостью и v2 с небольшой емкостью, а v2 скопирован на v1 (v1 = v2), большая емкость в v1 сохраняется после назначения; это имеет смысл, так как в следующих вызовах v1.push_back() нет необходимости форсировать новые перераспределения (другими словами: освобождение уже доступной памяти, а затем перераспределение ее для увеличения вектора не имеет большого смысла).

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

Если идиома копирования и свопинга не используется, а copy operator= и move operator= реализованы отдельно, тогда поведение будет таким же, как ожидалось (как для обычных нечленов vector s).

Почему? Должны ли мы не следовать идиоме "копирование и своп" и вместо этого использовать operator=(const X& other) (copy op=) и operator=(X&& other) (move op=) отдельно для оптимальной производительности?

Это результат воспроизводимого теста с идиомой копирования и смены (обратите внимание, как в этом случае после x1 = x2, x1.GetV().capacity() равно 1000, а не 1 000 000):

C:\TEMP\CppTests>cl /EHsc /W4 /nologo /DTEST_COPY_AND_SWAP test.cpp
test.cpp

C:\TEMP\CppTests>test.exe
v1.capacity() = 1000000
v2.capacity() = 1000

After copy v1 = v2:
v1.capacity() = 1000000
v2.capacity() = 1000

[Copy-and-swap]

x1.GetV().capacity() = 1000000
x2.GetV().capacity() = 1000

After x1 = x2:
x1.GetV().capacity() = 1000
x2.GetV().capacity() = 1000

Это вывод без идиомы копирования и свопинга (обратите внимание, как в этом случае x1.GetV().capacity() = 1000000, как ожидалось):

C:\TEMP\CppTests>cl /EHsc /W4 /nologo test.cpp
test.cpp

C:\TEMP\CppTests>test.exe
v1.capacity() = 1000000
v2.capacity() = 1000

After copy v1 = v2:
v1.capacity() = 1000000
v2.capacity() = 1000

[Copy-op= and move-op=]

x1.GetV().capacity() = 1000000
x2.GetV().capacity() = 1000

After x1 = x2:
x1.GetV().capacity() = 1000000
x2.GetV().capacity() = 1000

Далее следует компилятивный пример кода (протестирован с VS2010 SP1/VC10):

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

class X
{
public:
    X()
    {
    }

    explicit X(const size_t initialCapacity)
    {
        m_v.reserve(initialCapacity);
    }

    X(const X& other)
        : m_v(other.m_v)
    {
    }

    X(X&& other)
        : m_v(move(other.m_v))
    {
    }

    void SetV(const vector<double>& v)
    {
        m_v = v;
    }

    const vector<double>& GetV() const
    {
        return m_v;
    }

#ifdef TEST_COPY_AND_SWAP     
    //
    // Implement a unified op= with copy-and-swap idiom.
    //

    X& operator=(X other)
    {
        swap(*this, other);       
        return *this;
    }

    friend void swap(X& lhs, X& rhs)
    {
        using std::swap;

        swap(lhs.m_v, rhs.m_v);
    }    
#else    
    //
    // Implement copy op= and move op= separately.
    //

    X& operator=(const X& other)
    {
        if (this != &other)
        {
            m_v = other.m_v;
        }
        return *this;
    }

    X& operator=(X&& other)
    {
        if (this != &other)
        {
            m_v = move(other.m_v);
        }
        return *this;
    }    
#endif

private:
    vector<double> m_v;
};    

// Test vector assignment from a small vector to a vector with big capacity.
void Test1()
{
    vector<double> v1;
    v1.reserve(1000*1000);

    vector<double> v2(1000);

    cout << "v1.capacity() = " << v1.capacity() << '\n';
    cout << "v2.capacity() = " << v2.capacity() << '\n';

    v1 = v2;
    cout << "\nAfter copy v1 = v2:\n";    
    cout << "v1.capacity() = " << v1.capacity() << '\n';
    cout << "v2.capacity() = " << v2.capacity() << '\n';
}    

// Similar to Test1, but now vector is a data member inside a class.
void Test2()
{
#ifdef TEST_COPY_AND_SWAP 
    cout << "[Copy-and-swap]\n\n";
#else
    cout << "[Copy-op= and move-op=]\n\n";
#endif

    X x1(1000*1000);

    vector<double> v2(1000);
    X x2;
    x2.SetV(v2);

    cout << "x1.GetV().capacity() = " << x1.GetV().capacity() << '\n';
    cout << "x2.GetV().capacity() = " << x2.GetV().capacity() << '\n';

    x1 = x2;
    cout << "\nAfter x1 = x2:\n";
    cout << "x1.GetV().capacity() = " << x1.GetV().capacity() << '\n';
    cout << "x2.GetV().capacity() = " << x2.GetV().capacity() << '\n';
}

int main()
{
    Test1();       
    cout << '\n';    
    Test2();
}

Ответ 1

Скопировать и сменить с помощью std::vector может привести к потере производительности. Основная проблема здесь в том, что копирование std::vector включает в себя два разных этапа:

  • Выделить новый раздел памяти
  • Скопировать материал в.

Copy-and-swap может устранить # 2, но не # 1. Посмотрите, что вы наблюдали до вызова swap(), но после ввода задания op. У вас есть три вектора - тот, который должен быть перезаписан, тот, который является копией, и исходным аргументом.

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

Copy-and-swap - отличная базовая линия, особенно когда дело касается безопасности исключений, но это не глобально высокопроизводительное решение. Если вы находитесь в узкой области, то другие более специализированные реализации могут быть более эффективными, но следует предупредить, что исключение-безопасность в этой области является нетривиальной, а иногда и невозможной, если не скопировать и заменить.

Ответ 2

В случае X вы обмениваете векторы, не используя vector::operator=(). Назначение сохраняет емкость. swap пропускная способность.

Ответ 3

Если есть два вектора, скажем v1 с большой емкостью и v2 с малыми емкость и v2 копируется в v1 (v1 = v2), большая емкость в v1 равна хранится после задания; это имеет смысл,

Это не для меня.

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

Из быстрого сканирования стандарта я не уверен, что стандарт гарантирует, что емкость поддерживается постоянной при назначении из меньшего вектора. (Он будет храниться при вызове vector::assign(...), так что это может быть целью.)

Если я забочусь об эффективности памяти, во многих случаях мне нужно вызвать vector::shrink_to_fit() после назначения, если назначение не делает этого для меня.

Копирование и свопинг имеет семантику с усечением. На самом деле это была обычная идиома С++ 98 для термоусадки стандартных контейнеров.

поскольку следующие вызовы v1.push_back() не должны принудительно менять новые перераспределения (другими словами: освобождение уже имеющейся памяти, затем перераспределение он для роста вектор не имеет большого смысла).

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

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

Правда, если вы копируете и меняете в этом классе. Выполнение этого также скопирует и заменяет содержащиеся векторы, и, как упоминалось выше, это способ достижения сокращения.

Если идиома копирования и свопинга не используется, а оператор копирования = и перемещается operator = выполняются отдельно, тогда поведение такое же, как ожидалось (как для обычных нечленовских векторов).

Как обсуждалось выше: дискуссионно, является ли такое поведение ожидаемым.

Но если он соответствует вашим шаблонам использования, то есть, если вы хотите продолжить расти вектор после того, как он был назначен из другого, который мог быть меньше, чем предыдущее значение, тогда вы действительно можете получить некоторую эффективность, используя что-то, что не сбросить существующую избыточную емкость (например, vector::assign).

Почему? Если мы не будем придерживаться идиомы копирования и замены, а вместо этого реализовать operator = (const X & other) (copy op =) и operator = (X & & другое) (перемещение op =) отдельно для оптимальной производительности?

Как обсуждалось, если он соответствует вашему шаблону использования, и если производительность этой команды назначения и добавления является критичной, то вы действительно можете рассмотреть возможность использования swap и copy для назначения. Основная цель обмена и копирования - минимальная реализация (избегание дублированного кода) и высокая безопасность исключений.

Если вы выберете другую реализацию для максимальной производительности, вам придется самостоятельно заботиться о безопасности исключений, и вы заплатите цену за сложность кода.