10

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

如果有两个vectors,说v1大容量和v2小容量,v2复制到v1( v1 = v2),赋值后保留in中的大容量;v1这是有道理的,因为下一次v1.push_back()调用不必强制进行新的重新分配(换句话说:释放已经可用的内存,然后重新分配它以增加向量并没有多大意义)。

但是,如果对具有as 数据成员的vector进行相同的分配,则行为会有所不同,并且分配后不会保留更大的容量。

如果使用 copy-and-swap 习语,而 copyoperator=和 moveoperator=分开实现的,那么行为就如预期的那样(与普通的 non-member 一样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();
}
4

3 回答 3

12

使用 a 进行复制和交换std::vector确实会导致性能损失。这里的主要问题是复制 astd::vector涉及两个不同的阶段:

  1. 分配新的内存段
  2. 把东西复制进去。

复制和交换可以消除#2,但不能消除#1。考虑在 swap() 调用之前但在输入分配操作之后您会观察到什么。你有三个向量——一个即将被覆盖,一个是副本,一个是原始参数。

这清楚地表明,如果即将被覆盖的向量有足够的容量或过剩的容量,那么中间向量的创建就会浪费,而源的额外容量也会有所损失。其他容器也可以这样做。

复制和交换是一个很好的基准,尤其是在异常安全方面,但它并不是全球性能最高的解决方案。如果您在一个狭窄的领域,那么其他更专业的实现可能会更有效 - 但请注意,该领域的异常安全性并非易事,如果不进行复制和交换,有时甚至是不可能的。

于 2013-03-03T17:18:03.107 回答
5

在这种X情况下,您正在交换向量,而不是使用vector::operator=(). 分配保留容量。swap交换容量。

于 2013-03-03T17:19:18.900 回答
2

如果有两个向量,比如容量大的v1和容量小的v2,将v2复制到v1(v1=v2),赋值后保留v1中的大容量;这是有道理的,

对我来说不是。

分配后,我希望分配给向量具有与分配向量相同的值和状态。我为什么要招致并不得不拖累过剩的产能。

通过对标准的快速浏览,我不确定该标准是否保证容量在从较小向量的分配中保持不变。(它将在调用 时保留vector::assign(...),因此这可能是意图。)

如果我关心内存效率,vector::shrink_to_fit()在很多情况下我必须在分配后调用,如果分配不适合我。

复制和交换具有收缩以适应语义。实际上,它是标准容器收缩以适应的常用 C++98 习语。

因为下一个 v1.push_back() 调用不必强制进行新的重新分配(换句话说:释放已经可用的内存,然后重新分配它以增加向量并没有多大意义)。

没错,但这取决于您的使用模式。如果您分配向量然后继续添加它们,则保留任何预先存在的容量是有意义的。如果在构建其内容后分配向量,您可能不想保留分配的过多容量。

但是,如果对以向量作为数据成员的类进行相同的分配,则行为会有所不同,并且分配后不会保留更大的容量。

是的,如果您确实在该课程中进行复制和交换。这样做也将复制和交换包含的向量,如上所述,这是一种实现缩小以适应的方法。

如果不使用 copy-and-swap 习语,而 copy operator= 和 move operator= 分别实现,则行为符合预期(与普通非成员向量一样)。

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

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

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

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

如果您选择不同的实现以获得最佳性能,您将不得不自己处理异常安全,并且您将付出代码复杂性的代价。

于 2013-03-03T22:07:53.347 回答