一般来说,默认构造函数应该是创建空容器最快的方法。 这就是为什么我惊讶地发现它比初始化为空字符串文字更糟糕:
#include <string>
std::string make_default() {
return {};
}
std::string make_empty() {
return "";
}
编译为:(clang 16, libc++)
make_default():
mov rax, rdi
xorps xmm0, xmm0
movups xmmword ptr [rdi], xmm0
mov qword ptr [rdi + 16], 0
ret
make_empty():
mov rax, rdi
mov word ptr [rdi], 0
ret
请参阅编译器资源管理器中的实时示例。
注意返回
{}
总共将 24 个字节归零,但返回 ""
仅将 2 个字节归零。怎么return "";
好多了?
这是 libc++ 实现
std::string
时有意做出的决定。
首先,
std::string
具有所谓的小字符串优化(SSO),这意味着对于非常短(或空)的字符串,它将直接将其内容存储在容器内部,而不是分配动态内存。
这就是为什么我们在这两种情况下都看不到任何分配。
在 libc++ 中,
std::string
的“短表示”包括:
尺寸(x86_64) | 意义 |
---|---|
1位 | “短标志”表示它是一个短字符串(零表示是) |
7 位 | 字符串的长度,不包括空终止符 |
0 字节 | 填充字节以对齐字符串数据( 无) |
23字节 | 字符串数据,包括空终止符 |
对于空字符串,我们只需要存储两个字节的信息:
接受
const char*
的构造函数只会写入这两个字节,这是最少的。
默认构造函数 “不必要” 将 std::string
包含的所有 24 个字节归零。
总体来说这可能会更好,因为它使编译器可以发出 std::memset
或其他 SIMD 并行方法来批量对字符串数组进行清零。
有关完整说明,请参阅下文:
""
/ 呼叫 string(const char*)
要了解发生了什么,让我们看一下 std::basic_string
// constraints...
/* specifiers... */ basic_string(const _CharT* __s)
: /* leave memory indeterminate */ {
// assert that __s != nullptr
__init(__s, traits_type::length(__s));
// ...
}
这最终会调用
__init(__s, 0)
,其中0
是字符串的长度,从std::char_traits<char>
获得:
// template head etc...
void basic_string</* ... */>::__init(const value_type* __s, size_type __sz)
{
// length and constexpr checks
pointer __p;
if (__fits_in_sso(__sz))
{
__set_short_size(__sz); // set size to zero, first byte
__p = __get_short_pointer();
}
else
{
// not entered
}
traits_type::copy(std::__to_address(__p), __s, __sz); // copy string, nothing happens
traits_type::assign(__p[__sz], value_type()); // add null terminator
}
__set_short_size
最终只会写入一个字节,因为字符串的简短表示是:
struct __short
{
struct _LIBCPP_PACKED {
unsigned char __is_long_ : 1; // set to zero when active
unsigned char __size_ : 7; // set to zero for empty string
};
char __padding_[sizeof(value_type) - 1]; // zero size array
value_type __data_[__min_cap]; // null terminator goes here
};
编译器优化后,将
__is_long_
、__size_
归零,__data_
的一个字节编译为:
mov word ptr [rdi], 0
{}
/ 呼叫 string()
相比之下默认构造函数更加浪费:
/* specifiers... */ basic_string() /* noexcept(...) */
: /* leave memory indeterminate */ {
// ...
__default_init();
}
这最终会调用
__default_init()
,它会:
/* specifiers... */ void __default_init() {
__r_.first() = __rep(); // set representation to value-initialized __rep
// constexpr-only stuff...
}
__rep()
的值初始化会产生 24 个零字节,因为:
struct __rep {
union {
__long __l; // first union member gets initialized,
__short __s; // __long representation is 24 bytes large
__raw __r;
};
};
如果您想为了一致性而在各处进行值初始化,请不要因此而阻止您。不必要地清零一些字节并不是您需要担心的大性能问题。
事实上,在初始化大量字符串时它很有帮助,因为可以使用
std::memset
,或者其他一些将内存归零的SIMD方法。