不必要清空移出的 std::string
Unnecessary emptying of moved-from std::string
libstdc++ 和 libc++ 都使从 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 已经设置为 本地数据,所以我们只需为一个指针分配与之前分配相同的值。
(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)
}
}
在libc++的情况下,字符串移动构造函数确实清空了源代码,但并非没有必要。事实上,这个字符串实现的作者就是领导 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();
}
简而言之,此代码复制源的所有字节,然后将源的所有字节归零。需要立即注意的一件事:没有分支:此代码对长字符串和短字符串执行相同的操作。
长字符串模式
在"long mode"中,布局是3个字,一个数据指针和两个整数类型来存储大小和容量,long/short标志减去1位。加上分配器的 space(针对空分配器进行了优化)。
所以这会复制 pointer/sizes,然后清空源以释放指针的所有权。这还将源设置为 "short mode",因为 short/long 位表示在零状态下短路。此外,短模式中的所有零位表示一个零大小、非零容量的短字符串。
短字符串模式
当源是一个短字符串时,代码是相同的:字节被复制过来,源字节被清零。在短模式下没有自引用指针,因此复制字节是正确的算法。
现在确实 "short mode" 中的 3 个词的归零可能 似乎 没有必要,但要做到这一点必须检查long/short 位和零字节,在长模式下。由于偶尔的分支预测错误(破坏管道),执行此检查和分支实际上比仅将 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 [=11=], 16(%rsi)
movq [=11=], 8(%rsi)
movq [=11=], (%rsi)
movq %rdi, %rax
popq %rbp
retq
.cfi_endproc
(没有分支!)
<aside>
短字符串的内部缓冲区大小也针对移动成员进行了优化。内部缓冲区是 "union'ed","long mode" 需要 3 个字,因此 sizeof(string)
不需要比在长模式下更多的 space。尽管如此紧凑 sizeof
(3 个主要实现中最小的),libc++ 在 64 位架构上享有最大的内部缓冲区:22 char
.
小 sizeof
转化为更快的移动成员,因为所有这些成员所做的就是复制和零字节的对象布局。
有关内部缓冲区大小的更多详细信息,请参阅 this Whosebug answer。
</aside>
总结
所以总而言之,在 "long mode" 中需要将源设置为空字符串以转移指针的所有权,并且 在短模式下也需要 避免管道损坏的性能原因。
我对 libstdc++ 实现没有任何评论,因为我没有编写该代码,而且你的问题已经很好地完成了。 :-)
我知道我在实现 libstdc++ 版本时考虑过是否将移出的字符串置零,但我不记得我决定将其置零的原因。我想我可能决定将移出的字符串留空将遵循最小惊讶原则。移出字符串的大多数 "obvious" 状态是空的,即使 有时 为非空会稍微好一些。
正如评论中所建议的那样,它避免了破坏任何(可能是无意中)依赖于字符串为空的代码。我不认为这是我的考虑之一。依赖于 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);
}
}
libstdc++ 和 libc++ 都使从 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 已经设置为 本地数据,所以我们只需为一个指针分配与之前分配相同的值。
(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)
}
}
在libc++的情况下,字符串移动构造函数确实清空了源代码,但并非没有必要。事实上,这个字符串实现的作者就是领导 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();
}
简而言之,此代码复制源的所有字节,然后将源的所有字节归零。需要立即注意的一件事:没有分支:此代码对长字符串和短字符串执行相同的操作。
长字符串模式
在"long mode"中,布局是3个字,一个数据指针和两个整数类型来存储大小和容量,long/short标志减去1位。加上分配器的 space(针对空分配器进行了优化)。
所以这会复制 pointer/sizes,然后清空源以释放指针的所有权。这还将源设置为 "short mode",因为 short/long 位表示在零状态下短路。此外,短模式中的所有零位表示一个零大小、非零容量的短字符串。
短字符串模式
当源是一个短字符串时,代码是相同的:字节被复制过来,源字节被清零。在短模式下没有自引用指针,因此复制字节是正确的算法。
现在确实 "short mode" 中的 3 个词的归零可能 似乎 没有必要,但要做到这一点必须检查long/short 位和零字节,在长模式下。由于偶尔的分支预测错误(破坏管道),执行此检查和分支实际上比仅将 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 [=11=], 16(%rsi)
movq [=11=], 8(%rsi)
movq [=11=], (%rsi)
movq %rdi, %rax
popq %rbp
retq
.cfi_endproc
(没有分支!)
<aside>
短字符串的内部缓冲区大小也针对移动成员进行了优化。内部缓冲区是 "union'ed","long mode" 需要 3 个字,因此 sizeof(string)
不需要比在长模式下更多的 space。尽管如此紧凑 sizeof
(3 个主要实现中最小的),libc++ 在 64 位架构上享有最大的内部缓冲区:22 char
.
小 sizeof
转化为更快的移动成员,因为所有这些成员所做的就是复制和零字节的对象布局。
有关内部缓冲区大小的更多详细信息,请参阅 this Whosebug answer。
</aside>
总结
所以总而言之,在 "long mode" 中需要将源设置为空字符串以转移指针的所有权,并且 在短模式下也需要 避免管道损坏的性能原因。
我对 libstdc++ 实现没有任何评论,因为我没有编写该代码,而且你的问题已经很好地完成了。 :-)
我知道我在实现 libstdc++ 版本时考虑过是否将移出的字符串置零,但我不记得我决定将其置零的原因。我想我可能决定将移出的字符串留空将遵循最小惊讶原则。移出字符串的大多数 "obvious" 状态是空的,即使 有时 为非空会稍微好一些。
正如评论中所建议的那样,它避免了破坏任何(可能是无意中)依赖于字符串为空的代码。我不认为这是我的考虑之一。依赖于 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);
}
}