【问题标题】:On the implementation of std::string moves关于 std::string 动作的实现
【发布时间】:2018-11-08 06:06:20
【问题描述】:

我正在调查移动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

【问题讨论】:

  • 使用 SSO(短字符串优化)移动可以复制,因为您无法移动数组。
  • @NathanOliver 我知道,但 memcpy 涵盖了这一点。至少,我希望有一个大小为sizeof(string) 的缓冲区用于临时复制数组的内容,而不是创建临时字符串
  • 将`-stdlib=libc++`添加到clang窗口。
  • 你仍然在 libc++ 中发现了一个错误! basic_string::__clear_and_shrink() 应标记为 noexcept。有了这个添加,移动赋值运算符就可以很好地清理了。
  • __clear_and_shrink 显然是最近的编辑。我正在查看 libc++ 的尖端:github.com/llvm-mirror/libcxx/blob/master/include/…

标签: c++ string move-semantics


【解决方案1】:

我只分析了 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() 表示小字符串,描述here),则此举很便宜。

如果它是假的,那么它调用正常assign(不是移动的)。在以下任一情况下都会出现这种情况:

  • 字符串很小,所以assign 会做一个简单的memcpy(便宜)
  • 或者分配器没有特性always-equal也没有propagate-on-move-assign,所以分配器将分配 (不便宜)

这是什么意思?

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

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

【讨论】:

  • _Alloc_traits::_S_propagate_on_move_assign_Alloc_traits::_S_always_equalconstexpr 函数。不应该在编译时消除这些分支并且不存在于程序集中吗?
  • @MaximEgorushkin:它们不存在于程序集中。它首先在 locality 属性上分支(我在答案中包含的代码),因此必须编译 assign 代码。然后,assign 代码不检查 locality 属性,但它检查容量(从assign 的角度来看,这是可以理解的,因为那里的容量很重要,而不是位置)。并且编译器不够聪明(可以理解)将这两件事关联在一起,所以它将分配代码路径放入 exe 中。
  • 您可能要提到_M_is_local 用于小字符串优化。
【解决方案2】:

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

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


我不应该认为搬家几乎是免费的吗?

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

复制和移动的比较

  • 许多人错误地认为移动实际上是“免费的”
  • 复制和移动之间的性能差异差异很大
  • 对于基本类型,例如 int,复制或移动实际上是相同的
  • 当只需要转移对象的一部分来转移整个价值时,移动比复制快

【讨论】:

    猜你喜欢
    • 2013-11-01
    • 2010-11-30
    • 1970-01-01
    • 2018-10-23
    • 1970-01-01
    • 2015-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多