对于复制赋值运算符可以回收资源的类型,与副本进行交换几乎从来都不是实现复制赋值运算符的最佳方式。例如看看std::vector
:
此类管理动态大小的缓冲区并维护capacity
(缓冲区可以容纳的最大长度),以及size
(当前长度)。如果vector
复制赋值运算符已实现swap
,那么无论如何,如果rhs.size() != 0
.
然而,如果lhs.capacity() >= rhs.size()
,根本不需要分配新的缓冲区。人们可以简单地分配/构造元素rhs
to lhs
。当元素类型是普通可复制的时,这可能归结为什么都没有,但memcpy
。这可以很多,much比分配和取消分配缓冲区更快。
同样的问题std::string
.
同样的问题MyType
when MyType
具有以下数据成员std::vector
and/or std::string
.
只有 2 次您需要考虑使用交换来实现复制分配:
你知道swap
方法(包括当 rhs 是左值时的强制复制构造)不会非常低效。
你知道你会always需要复制赋值运算符有较强的异常安全保证。
如果您不确定 2,换句话说您认为复制赋值运算符可能有时需要强有力的异常安全保障,不要以交换的方式实现赋值。如果您提供以下之一,您的客户很容易获得相同的保证:
- 无例外交换。
- noexcept 移动赋值运算符。
例如:
template <class T>
T&
strong_assign(T& x, T y)
{
using std::swap;
swap(x, y);
return x;
}
or:
template <class T>
T&
strong_assign(T& x, T y)
{
x = std::move(y);
return x;
}
现在,在某些类型中,通过交换实现复制分配是有意义的。然而,这些类型只是例外,而不是规则。
On:
void push_back(const value_type& val);
void push_back(value_type&& val);
Imagine vector<big_legacy_type>
where:
class big_legacy_type
{
public:
big_legacy_type(const big_legacy_type&); // expensive
// no move members ...
};
如果我们只有:
void push_back(value_type val);
Then push_back
计算左值big_legacy_type
into a vector
需要 2 份而不是 1 份,即使capacity
就足够了。从性能角度来看,这将是一场灾难。
Update
这是一个 HelloWorld,您应该能够在任何符合 C++11 的平台上运行:
#include <vector>
#include <random>
#include <chrono>
#include <iostream>
class X
{
std::vector<int> v_;
public:
explicit X(unsigned s) : v_(s) {}
#if SLOW_DOWN
X(const X&) = default;
X(X&&) = default;
X& operator=(X x)
{
v_.swap(x.v_);
return *this;
}
#endif
};
std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);
std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
auto t0 = std::chrono::high_resolution_clock::now();
x = y;
auto t1 = std::chrono::high_resolution_clock::now();
return t1-t0;
}
int
main()
{
const int N = 1000000;
typedef std::chrono::duration<double, std::nano> nano;
nano ns(0);
for (int i = 0; i < N; ++i)
{
X x1(size(eng));
X x2(size(eng));
ns += test(x1, x2);
}
ns /= N;
std::cout << ns.count() << "ns\n";
}
我已经编码了X
的复制赋值运算符有两种方式:
- 隐式的,相当于调用
vector
的复制赋值运算符。
- 用复制/交换惯用语,建议在宏下
SLOW_DOWN
。我想过给它命名SLEEP_FOR_AWHILE
,但如果您使用的是电池供电的设备,这种方式实际上比睡眠语句要糟糕得多。
该测试构建了一些随机大小的vector<int>
s 介于 0 和 1000 之间,并对它们赋值一百万次。它对每个时间进行计时,将时间相加,然后找到以浮点纳秒为单位的平均时间并将其打印出来。如果对高分辨率时钟的两次连续调用没有返回小于 100 纳秒的时间,您可能需要增加向量的长度。
这是我的结果:
$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp
$ a.out
428.348ns
$ a.out
438.5ns
$ a.out
431.465ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp
$ a.out
617.045ns
$ a.out
616.964ns
$ a.out
618.808ns
通过这个简单的测试,我发现复制/交换习惯的性能下降了 43%。 YMMV。
平均而言,上述测试在一半的时间里 lhs 具有足够的容量。如果我们采取任一极端:
- lhs始终有足够的容量。
- lhs 始终没有足够的容量。
那么默认复制分配相对于复制/交换习惯的性能优势从大约 560% 到 0% 不等。复制/交换习惯永远不会更快,并且可能会显着变慢(对于此测试)。
想要速度吗?措施。