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)
对于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 ++实现发表评论,因为我没有编写代码,你的问题无论如何已经做得很好.:-)