提问人:Jan Schultke 提问时间:6/26/2023 更新时间:9/23/2023 访问量:4479
为什么将字符串初始化为 “” 比默认构造函数更有效?
Why is initializing a string to "" more efficient than the default constructor?
问:
通常,默认构造函数应该是创建空容器的最快方法。 这就是为什么我惊讶地发现它比初始化为空字符串文字更糟糕:
#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
首先,有所谓的小字符串优化(SSO),这意味着对于非常短(或空)的字符串,它将直接将其内容存储在容器内,而不是分配动态内存。
这就是为什么我们在这两种情况下都看不到任何分配的原因。std::string
在 libc++ 中,a 的“短表示”包括:std::string
尺寸 (x86_64) | 意义 |
---|---|
1 位 | “short flag”表示它是短字符串(零表示是) |
7 位 | 字符串的长度,不包括 null 终止符 |
0 字节 | 填充字节以对齐字符串数据(无basic_string<char> ) |
23 字节 | 字符串数据,包括 null 终止符 |
对于空字符串,我们只需要存储两个字节的信息:
- 一个零字节用于“短标志”和长度
- 一个零字节用于 null 终止符
接受 a 的构造函数将只写入这两个字节,这是最小的字节。
默认构造函数“不必要地”将包含的所有 24 个字节归零。不过,总体上这可能更好,因为它使编译器可以发出 std::memset
或其他 SIMD 并行方式批量清零字符串数组。const char*
std::string
有关完整说明,请参见下文:
初始化为/调用""
string(const char*)
为了理解会发生什么,让我们看一下 std::basic_string
的 libc++ 源代码:
// 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
};
编译器优化后,将 、 和 1 个字节编译为:__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...
}
a 的值初始化结果为 24 个零字节,因为:__rep()
struct __rep {
union {
__long __l; // first union member gets initialized,
__short __s; // __long representation is 24 bytes large
__raw __r;
};
};
结论
如果你想为了一致性而到处进行值初始化,不要让它阻止你。不必要地将几个字节归零并不是您需要担心的大性能问题。
事实上,它在初始化大量字符串时很有帮助,因为可能会使用或其他一些 SIMD 方式将内存归零。std::memset
评论
std::string
memset
""
mov ptr addr, 0
""
mov
std::vector
std::vector<std::string>
评论
std::string
make_empty()
make_default()
{}
""
char s[100] = "Hello";
'h', 'e', 'l', 'l', 'o', '\0'
char s[100] = {'h', 'e', 'l', 'l', 'o', '\0'};