我正在调查移动
std::string
的性能。在很长一段时间里,我认为字符串移动几乎是免费的,认为编译器会内联所有内容,并且只涉及一些廉价的赋值。
事实上,我对于移动的心理模型就是字面上的
string& operator=(string&& rhs) noexcept
{
swap(*this, rhs);
return *this;
}
friend void swap(string& x, string& y) noexcept
{
// exposition only
unsigned char buf[sizeof(string)];
memcpy(buf, &x, sizeof(string));
memcpy(&x, &y, sizeof(string));
memcpy(&y, buf, sizeof(string));
}
据我所知,如果将
memcpy
更改为分配各个字段,这就是合法的实现。
令我非常惊讶的是,发现 gcc 的移动实现涉及 创建一个新字符串 并且 可能会由于分配而抛出,尽管是
noexcept
。
这符合吗?同样重要的是,我不应该认为搬家几乎是免费的吗?
std::vector<char>
编译下来达到了我的预期。
clang 的实现有很大不同,尽管有一个可疑的
std::string::reserve
我只分析了GCC的版本。发生的事情是这样的:代码处理不同类型的分配器。如果分配器具有
_S_propagate_on_move_assign
或 _S_always_equal
的特征,那么正如您所期望的,移动几乎是免费的。这是移动中的if
operator=
:
if (!__str._M_is_local()
&& (_Alloc_traits::_S_propagate_on_move_assign()
|| _Alloc_traits::_S_always_equal()))
// cheap move
else assign(__str);
如果条件为真(
_M_is_local()
表示小字符串,描述这里),那么此举很便宜。
如果为 false,则调用 normal
assign
(不是移动的)。出现以下任一情况时就会出现这种情况:
assign
会做一个简单的memcpy(便宜)这是什么意思?
这意味着,如果您使用默认分配器(或任何具有前面提到的特征的分配器),那么 move 仍然几乎是免费的。
另一方面,生成的代码不必要地庞大,我认为可以改进。它应该有一个单独的代码来处理常用的分配器,或者有一个更好的
assign
代码(问题是 assign
不检查 _M_is_local()
,但它会进行容量检查,因此编译器无法决定是否存在是否需要分配,因此它不必要地将分配代码路径放入可执行文件中 - 您可以在源代码中查看确切的详细信息)。
不完全是一个答案,但这是 C++11 的新实现
std::string
,没有引用计数器,并且采用小字符串优化,导致大量汇编。特别是,小字符串优化导致 4 个分支处理移动分配的源和目标长度的 4 种不同组合。
添加
-D_GLIBCXX_USE_CXX11_ABI=0
选项以使用带有引用计数器且没有小字符串优化的 pre C++-11 std::string
汇编代码看起来好多了。
我不应该认为搬家几乎是免费的吗?
在 Roger Orr 的没有什么比复制或移动更好的演讲中,幻灯片第 47 页上写道:
复制与移动对比
- 许多人错误地认为搬家实际上是“免费”的
- 复制和移动之间的性能差异差异很大
- 对于原始类型,例如 int,复制或移动实际上是相同的
- 当只需要传输对象的一部分来传输整个值时,移动比复制更快