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)
}
}
答案 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);
}
}