【问题标题】:Unnecessary emptying of moved-from std::string从 std::string 移出的不必要的清空
【发布时间】:2018-10-08 06:18:11
【问题描述】:

libstdc++ 和 libc++ 都使 move-from std::string 对象为空,即使原始存储的字符串很短并且应用了短字符串优化。在我看来,这种清空会产生额外且不必要的运行时开销。例如,这里是来自 libstdc++ 的 std::basic_string 的移动构造函数:

basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) {
    if (__str._M_is_local()) 
      traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1);
    else {
      _M_data(__str._M_data());
      _M_capacity(__str._M_allocated_capacity);
    }
    _M_length(__str.length());
    __str._M_data(__str._M_local_data());  // (1)
    __str._M_set_length(0);                // (2)
  }

(1) 是一个在短字符串情况下无用的赋值,因为 data 已经设置为 local data,所以我们只是分配一个指针,它与之前分配的值相同。

(2) 清空字符串设置字符串大小并重置本地缓冲区中的第一个字符,据我所知,标准没有要求

通常,库实现者会尝试尽可能高效地实现标准(例如,已删除的内存区域不会清零)。 我的问题是,是否有任何特殊原因导致移出的字符串被清空,即使它不是必需的,并且会增加不必要的开销。其中,可以很容易地消除,例如,通过:

basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) {
    if (__str._M_is_local()) {
      traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1);
      _M_length(__str.length());
    }
    else {
      _M_data(__str._M_data());
      _M_capacity(__str._M_allocated_capacity);
      _M_length(__str.length());
      __str._M_data(__str._M_local_data());  // (1)
      __str._M_set_length(0);                // (2)
    }
  }

【问题讨论】:

  • 这也许是对遗留代码的致敬。在 SSO 进入 libstdc++ 之前,std::string 已被引用计数,并且所有移出字符串为空。旧代码可能(错误地)依赖它。
  • 顺便说一句,在您建议的替换中,在使用它来设置目标长度之前,您不能将源长度设置为零。
  • @j6t 已更新,感谢您指出这一点。
  • operator=相同。

标签: c++ string move-semantics libstdc++


【解决方案1】:

在 libc++ 的情况下,字符串移动构造函数确实清空了源代码,但这不是不必要的。事实上,这个字符串实现的作者正是领导 C++11 移动语义提案的人。 ;-)

这个libc++字符串的实现其实是从成员向外移动设计的!

以下是省略了一些不必要的细节(如调试模式)的代码:

template <class _CharT, class _Traits, class _Allocator>
basic_string<_CharT, _Traits, _Allocator>::basic_string(basic_string&& __str)
        _NOEXCEPT
    : __r_(_VSTD::move(__str.__r_))
{
    __str.__zero();
}

简而言之,此代码复制源的所有字节,然后将源的所有字节归零。需要立即注意的一件事:没有分支:此代码对长字符串和短字符串执行相同的操作。

长字符串模式

在“长模式”中,布局是 3 个字、一个数据指针和两个用于存储大小和容量的整数类型,长/短标志减去 1 位。加上分配器的空间(针对空分配器进行了优化)。

所以这会复制指针/大小,然后将源清空以释放指针的所有权。这也将源设置为“短模式”,因为短/长位意味着在零状态下是短的。此外,短模式中的所有零位都表示零大小、非零容量的短字符串。

短字符串模式

当源是一个短字符串时,代码是相同的:字节被复制,源字节被清零。在短模式下没有自引用指针,因此复制字节是正确的算法。

现在确实,在“短模式”中,源的 3 个字的归零可能似乎没有必要,但要做到这一点,必须检查长/短位和零长模式下的字节数。由于偶尔的分支预测错误(中断管道),执行此检查和分支实际上比仅将 3 个单词归零更昂贵。

这是针对 libc++ string 移动构造函数的优化 x86(64 位)程序集。

std::string
test(std::string& s)
{
    return std::move(s);
}

__Z4testRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z4testRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    16(%rsi), %rax
    movq    %rax, 16(%rdi)
    movq    (%rsi), %rax
    movq    8(%rsi), %rcx
    movq    %rcx, 8(%rdi)
    movq    %rax, (%rdi)
    movq    $0, 16(%rsi)
    movq    $0, 8(%rsi)
    movq    $0, (%rsi)
    movq    %rdi, %rax
    popq    %rbp
    retq
    .cfi_endproc

(没有分支!)

&lt;aside&gt;

短字符串的内部缓冲区大小也针对移动成员进行了优化。内部缓冲区与“长模式”所需的 3 个字“联合”,因此sizeof(string) 不需要比长模式更多的空间。尽管有这个紧凑的sizeof(3 个主要实现中最小的),但 libc++ 在 64 位架构上享有最大的内部缓冲区:22 char

sizeof 转换为更快的移动成员,因为所有这些成员所做的只是复制对象布局的零字节。

有关内部缓冲区大小的更多详细信息,请参阅this Stackoverflow answer

&lt;/aside&gt;

总结

因此,总而言之,在“长模式”中将源设置为空字符串是必要的,以转移指针的所有权,在短模式下出于性能原因以避免损坏管道。

我对 libstdc++ 的实现没有任何评论,因为我没有编写该代码,而且你的问题已经很好地解决了这个问题。 :-)

【讨论】:

  • 感谢有关 libc++ 的详细解答。我熟悉这两种实现,并且 libstdc++ 使用一个额外的缓冲区(“联合”与容量),它的优点是在访问存储的字符串时没有分支(在 libc++ 中,必须检查位标志)。您写的内容以及@j6t 评论非常有意义。
【解决方案2】:

我知道我在实现 libstdc++ 版本时考虑过是否将已移动的字符串归零,但我不记得我决定将其归零的原因。我想我可能决定将移出的字符串留空将遵循最小惊讶原则。移出字符串最“明显”的状态是为空,即使 sometimes 为非空会稍微好一些。

正如 cmets 中所建议的,它避免破坏任何(可能是无意的)依赖于空字符串的代码。我不认为这是我的考虑之一。依赖于 COW 字符串语义的 C++11 代码不仅会被非空的移动字符串破坏。

值得注意的是,与您建议的替代方案相比,在-O2 处,当前的 libstdc++ 代码编译为更少的指令。然而,像这样的东西编译得更小,而且可能更快(虽然我没有测量它,甚至没有测试它是否有效):

  basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator()))
  {
    memcpy(_M_local_buf, __str._M_local_buf, sizeof(_M_local_buf));
    _M_length(__str.length());
    if (!__str._M_is_local())
      {
        _M_data(__str._M_data());
        __str._M_data(__str._M_local_data());
        __str._M_set_length(0);
      }
  }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-26
    • 2021-04-14
    • 1970-01-01
    相关资源
    最近更新 更多