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

Я тестировал некоторый код, где есть 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();
}

10

Решение

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

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

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

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

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

12

Другие решения

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

5

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2