不必要的清空移动的std :: string

时间:2018-10-08 06:18:11

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

libstdc ++和libc ++都将从std::string对象移出为空,即使原始存储的字符串很短并且已应用了短字符串优化也是如此。在我看来,这种清空会增加不必要的运行时开销。例如,下面是libstdc ++中std::basic_string的move构造函数:

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 已设置为本地数据,因此我们只需为指针分配与之前分配的值相同的值即可。

(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)
    }
  }

2 个答案:

答案 0 :(得分:5)

对于libc ++,字符串move构造函数不会清空源,但不是必须的。确实,此字符串实现的作者是领导C ++ 11的移动语义建议的同一人。 ;-)

这个libc ++字符串的实现实际上是从move成员向外设计的!

以下是带有一些不必要的详细信息(例如调试模式)的代码:

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 move构造函数的优化的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

(没有分支!)

<aside>

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

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

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

</aside>

摘要

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

我没有评论libstdc ++的实现,因为我没有编写该代码,而且您的问题无论如何已经做得很好。 :-)

答案 1 :(得分:2)

我知道我在实现libstdc ++版本时曾考虑过是否将移出的字符串归零,但是我不记得决定将其归零的原因。我想我可能已经决定,将移出字符串保留为空将遵循最小惊讶原则。即使从有时为非空的情况下,移出的字符串最“明显”的状态为空。

正如注释中所建议的那样,它避免了破坏任何依赖于字符串为空的代码(可能是无意的)。我认为这不是我的考虑因素之一。依赖于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);
      }
  }