关于执行 std::string 步

On the implementation of std::string moves

我正在调查移动 std::string 的性能。很长一段时间以来,我一直认为字符串移动几乎是免费的,认为编译器会内联所有内容,并且只会涉及一些廉价的分配。

事实上,我的搬家心智模型就是

string& operator=(string&& rhs) noexcept
{
    swap(*this, rhs);
    return *this;
}

friend void swap(string& x, string& y) noexcept
{
    // for disposition only
    unsigned char buf[sizeof(string)];
    memcpy(buf, &x, sizeof(string));
    memcpy(&x, &y, sizeof(string));
    memcpy(&y, buf, sizeof(string));
}

据我所知,如果 memcpy 更改为分配单个字段,这是合法的实施。

我很惊讶地发现 gcc 的移动实现涉及 creating a new string and might possibly throw due to the allocations despite being noexcept

这还合规吗?同样重要的是,我不应该认为搬家几乎是免费的吗?


令人困惑的是,std::vector<char> compiles down 符合我的预期。

clang's implementation就大不一样了,虽然有疑点std::string::reserve

不完全是一个答案,但这是 C++11 std::string 的新实现,没有引用计数器和小字符串优化,导致大量汇编。特别是,小字符串优化导致 4 个分支处理移动分配的源和目标长度的 4 种不同组合。

当添加 -D_GLIBCXX_USE_CXX11_ABI=0 选项以使用带有引用计数器的预 C++-11 std::string 并且没有小字符串优化时 the assembly code looks much better


should I not think moving is almost free?

Nothing is Better than Copy or Move by Roger Orr talk, slides 第 47 页中说:

Comparison of copy and move

  • Many people incorrectly think of move as effectively 'free'
  • The performance difference between copy and move varies widely
  • For a primitive type, such as int, copying or moving are effectively identical
  • Move is faster than copy when only part of the object needs to be transferred to transfer the whole value

我只分析了GCC的版本。事情是这样的:代码处理不同类型的分配器。如果分配器具有 _S_propagate_on_move_assign_S_always_equal 的特征,那么移动几乎是免费的,如您所料。这是移动 operator=:

中的 if
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()表示小串,说明here),则走法便宜

如果为假,则调用正常 assign(不是移动的)。这是以下情况之一:

  • 字符串很小,所以assign会做一个简单的memcpy(便宜)
  • 或者分配器不具有 always-equalpropagate-on-move-assign 特征,因此分配 会分配(不便宜)

这是什么意思?

这意味着,如果您使用默认分配器(或具有前面提到的特征的任何分配器),那么移动仍然几乎是免费的

另一方面,生成的代码不必要地庞大,我认为可以改进。它应该有一个单独的代码来处理通常的分配器,或者有一个更好的 assign 代码(问题是 assign 不检查 _M_is_local(),但它会检查容量,所以编译器无法决定是否需要分配,因此它不必要地将分配代码路径放入可执行文件中 - 您可以在源代码中查看确切的详细信息。