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

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

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

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

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

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

Это вывод воспроизводимого теста с идиомой копирования и замены (обратите внимание, как в этом случае после x1 = x2, x1.GetV().capacity() 1 000, а не 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();
}

3 ответа

Решение

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

  1. Выделите новый раздел памяти
  2. Скопируйте вещи в.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как уже говорилось выше: это спорно ли это поведение, как и ожидалось.

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

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

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

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

Другие вопросы по тегам