This answer给出了短字符串优化(SSO)的高级概述。但是,我想更详细地了解它在实践中是如何工作的,特别是在libc ++实现中:
为了符合SSO资格,字符串必须有多短? 这取决于目标架构吗?
实施如何区分短期和长期
访问字符串数据时的字符串?它是否像m_size <= 16
一样简单,还是一个标志,是其他成员变量的一部分? (一世
想象m_size
或其中的一部分也可能用于存储
字符串数据)。
我专门针对libc ++提出了这个问题,因为我知道它使用了SSO,甚至在libc++ home page上也提到过。
以下是the source后的一些观察结果:
libc ++可以使用两个稍微不同的字符串类内存布局进行编译,这由_LIBCPP_ALTERNATE_STRING_LAYOUT
标志控制。这两种布局还区分了little-endian和big-endian机器,这些机器总共留下了4种不同的变体。我将在下面的内容中假设“正常”布局和小端。
进一步假设size_type
是4个字节且value_type
是1个字节,这就是字符串的前4个字节在内存中的样子:
// short string: (s)ize and 3 bytes of char (d)ata
sssssss0;dddddddd;dddddddd;dddddddd
^- is_long = 0
// long string: (c)apacity
ccccccc1;cccccccc;cccccccc;cccccccc
^- is_long = 1
由于短字符串的大小在高7位,因此在访问时需要移位:
size_type __get_short_size() const {
return __r_.first().__s.__size_ >> 1;
}
类似地,长字符串容量的getter和setter使用__long_mask
来处理is_long
位。
我仍在寻找我的第一个问题的答案,即短字符串的容量__min_cap
对于不同的架构会有什么价值?
其他标准库实施
This answer概述了其他标准库实现中的std::string
内存布局。
答案 0 :(得分:105)
libc ++ basic_string
旨在在所有体系结构上都有sizeof
个3个字,其中sizeof(word) == sizeof(void*)
。您已正确解剖了长/短标志,以及缩写形式的大小字段。
短字符串的容量__min_cap对不同的架构有什么价值?
在简短形式中,有3个词可供使用:
char
,1个字节进入尾随空(libc ++将始终在数据后面存储尾随空值。)这留下3个字减去2个字节来存储一个短字符串(即没有分配的最大capacity()
)。
在32位机器上,10个字符将适合短字符串。 sizeof(string)是12。
在64位机器上,22个字符将适合短字符串。 sizeof(string)是24。
主要设计目标是最小化sizeof(string)
,同时使内部缓冲区尽可能大。理由是加快移动建设和移动任务。 sizeof
越大,在移动构造或移动分配期间您必须移动的单词越多。
长格式需要至少3个字来存储数据指针,大小和容量。因此我将简短形式限制为相同的3个单词。有人建议4字大小可能有更好的表现。我没有测试过那种设计选择。
<强> _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT 强>
有一个名为_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT
的配置标志,它重新排列数据成员,使“长布局”发生变化:
struct __long
{
size_type __cap_;
size_type __size_;
pointer __data_;
};
为:
struct __long
{
pointer __data_;
size_type __size_;
size_type __cap_;
};
这种变化的动机是相信,__data_
首先将由于更好的对齐而具有一些性能优势。试图衡量性能优势,很难衡量。它不会使性能变差,而且可能会稍微好一些。
应谨慎使用旗帜。它是一个不同的ABI,如果意外地与使用不同设置std::string
编译的libc ++ _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT
混合将产生运行时错误。
我建议只有libc ++的供应商才能更改此标志。
答案 1 :(得分:19)
libc++ implementation有点复杂,我会忽略它的替代设计并假设有一个小端计算机:
template <...>
class basic_string {
/* many many things */
struct __long
{
size_type __cap_;
size_type __size_;
pointer __data_;
};
enum {__short_mask = 0x01};
enum {__long_mask = 0x1ul};
enum {__min_cap = (sizeof(__long) - 1)/sizeof(value_type) > 2 ?
(sizeof(__long) - 1)/sizeof(value_type) : 2};
struct __short
{
union
{
unsigned char __size_;
value_type __lx;
};
value_type __data_[__min_cap];
};
union __ulx{__long __lx; __short __lxx;};
enum {__n_words = sizeof(__ulx) / sizeof(size_type)};
struct __raw
{
size_type __words[__n_words];
};
struct __rep
{
union
{
__long __l;
__short __s;
__raw __r;
};
};
__compressed_pair<__rep, allocator_type> __r_;
}; // basic_string
注意:__compressed_pair
基本上是针对Empty Base Optimization优化的对,又名template <T1, T2> struct __compressed_pair: T1, T2 {};
;对于所有意图和目的,你可以认为它是一个常规对。它的重要性刚刚出现,因为std::allocator
是无国籍的,因此是空的。
好的,这是相当原始的,所以让我们检查一下这些机制!在内部,许多函数会调用__get_pointer()
,它自己调用__is_long
来确定字符串是使用__long
还是__short
表示:
bool __is_long() const _NOEXCEPT
{ return bool(__r_.first().__s.__size_ & __short_mask); }
// __r_.first() -> __rep const&
// .__s -> __short const&
// .__size_ -> unsigned char
说实话,我不太确定这是标准C ++(我知道union
中的初始子序列规定,但不知道它是如何与匿名联合和别名一起抛出的),而是一个标准库允许无论如何都要利用实现定义的行为。