复制-交换习语的低效率

Inefficiency of copy-and-swap idiom?

本文关键字:低效率 习语 交换 复制      更新时间:2023-10-16

我正在测试一些代码,其中在类中有一个std::vector数据成员。该类既是可复制的又是可移动的,并且operator=是使用复制和交换习语实现的。

如果存在两个vector,例如容量大的v1和容量小的v2,将v2拷贝到v1 (v1 = v2)中,则分配后保留v1中的大容量;这是有意义的,因为下一个v1.push_back()调用不必强制新的重新分配(换句话说:释放已经可用的内存,然后重新分配它来增长向量没有多大意义)。

但是,如果将vector作为数据成员的进行相同的赋值,则行为不同,赋值后较大的容量是而不是

如果没有使用复制和交换习惯用法,并且copy operator=和move operator=分别实现,则行为是预期的(与普通非成员vector一样)。

为什么?我们是否应该不遵循复制和交换的习惯用法,而是分别实现operator=(const X& other) (copy op=)和operator=(X&& other) (move op=)以获得最佳性能?

这是使用复制和交换习惯的可重复测试的输出(注意在本例中,x1 = x2之后,x1.GetV().capacity()是1,000,而不是1,000,000):

C:TEMPCppTests>cl /EHsc /W4 /nologo /DTEST_COPY_AND_SWAP test.cpp
test.cpp
C:TEMPCppTests>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:TEMPCppTests>cl /EHsc /W4 /nologo test.cpp
test.cpp
C:TEMPCppTests>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]nn";
#else
    cout << "[Copy-op= and move-op=]nn";
#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();
}

使用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惯用用法。

因为next v1.push_back()调用不必强制新的重新分配(换句话说:释放已经可用的内存,然后重新分配

没错,但这取决于你的使用模式。如果您分配向量,然后继续对它们进行添加,那么保留任何已有的容量是有意义的。如果在构建了vector的内容之后才对其进行赋值,则可能不希望继续分配多余的容量。

但是,如果对具有vector的类执行相同的赋值操作作为数据成员,其行为与赋值后不同大容量不保留

True,如果在该类中执行复制和交换操作。这样做还会复制和交换包含的向量,正如上面提到的,这是实现收缩到适合的一种方法。

如果不使用复制和交换习惯,则复制操作符=和moveOperator =分别实现,则行为如预期的那样(对于普通非成员向量)

如上所述,这种行为是否符合预期是有争议的。

但是如果它符合你的使用模式,也就是说,如果你想在从另一个可能小于先前值的向量中分配一个向量后继续增长,那么你确实可以通过使用一些不会减少现有过剩容量的东西来获得一些效率(例如vector::assign)。

为什么?我们不应该遵循复制-交换的习惯用法吗实现operator=(const X&其他)(复制op=)和操作符=(X&&其他)(移动op=)分开以获得最佳性能?

如前所述,如果

符合您的使用模式,并且如果赋值和追加序列的性能至关重要,那么您确实可以考虑不使用交换和复制进行赋值。交换和复制的主要目的是最小化实现(避免重复代码)和强异常安全性。

如果你选择一个不同的实现来获得最大的性能,你将不得不自己照顾异常安全,你将付出代码复杂性的代价。