从std :: string中不必要地清空move-from

Dan*_*ica 9 c++ string libstdc++ move-semantics

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

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)
  }
Run Code Online (Sandbox Code Playgroud)

(1)是一个在短字符串情况下无效的赋值,因为数据已经设置为本地数据,所以我们只需为指针指定与之前分配的值相同的值.

(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)
    }
  }
Run Code Online (Sandbox Code Playgroud)

How*_*ant 8

对于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();
}
Run Code Online (Sandbox Code Playgroud)

简而言之,此代码复制源的所有字节,然后将源的所有字节归零.有一点要立即注意:没有分支:这个代码对长字符串和短字符串做同样的事情.

长串模式

在"长模式"中,布局为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
Run Code Online (Sandbox Code Playgroud)

(没有分支!)

<aside>

短字符串的内部缓冲区的大小也针对移动成员进行了优化.内部缓冲器是"union'ed"与"长模式"所需要的3个字,使得sizeof(string)需要在长模式中比在没有更多的空间.尽管这个紧凑sizeof(在3个主要实现中最小),但libc ++享有64位架构上最大的内部缓冲区:22 char.

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

有关内部缓冲区大小的更多详细信息,请参阅此Stackoverflow答案.

</aside>

摘要

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

我没有对libstdc ++实现发表评论,因为我没有编写代码,你的问题无论如何已经做得很好.:-)