libc ++中短字符串优化的机制是什么?


102

该答案很好地概述了短字符串优化(SSO)。但是,我想更详细地了解它在实践中如何工作,特别是在libc ++实现中:

  • 要符合SSO要求,字符串必须多短?这取决于目标体系结构吗?

  • 访问字符串数据时,实现如何区分短字符串和长字符串?它是否像m_size <= 16其他成员变量中的一部分一样简单?(我想它的m_size一部分或一部分也可以用来存储字符串数据)。

我专门针对libc ++提出了这个问题,因为我知道它使用SSO,甚至在libc ++主页上也提到了这一点。

查看来源后,有一些观察结果:

libc ++可以为字符串类使用两个略有不同的内存布局进行编译,这由_LIBCPP_ALTERNATE_STRING_LAYOUT标志控制。两种布局还区分了小端和大端机器,这使我们总共有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对于不同的体系结构,短字符串的容量将获得什么价值?

其他标准库实现

该答案很好地概述了std::string其他标准库实现中的内存布局。


libc ++是开源的,您可以在这里找到其string标题,我目前正在检查中:)
Matthieu M. 2014年


@Matthieu M .:之前我已经看到过,不幸的是,这是一个很大的文件,感谢您帮助我们检出它。
ValarDohaeris 2014年

@Ali:我在谷歌搜索中偶然发现了这个。但是,此博客文章明确指出,这仅是SSO的示例,并不是在实践中使用的高度优化的变体。
ValarDohaeris 2014年

Answers:


120

libc ++ basic_string设计为sizeof在所有体系结构上都有3个单词,其中sizeof(word) == sizeof(void*)。您已经正确剖析了long / short标志和short格式的size字段。

__min_cap(短字符串的容量)对于不同的体系结构将取什么值?

简而言之,可以使用3个词:

  • 1位进入多头/空头标志。
  • 7位大小。
  • 假设char尾随null为1个字节(libc ++将始终在数据后面存储尾随null)。

这样就剩下3个字减去2个字节来存储一个短字符串(即最大的capacity()未分配空间)。

在32位计算机上,短字符串中可容纳10个字符。sizeof(string)是12。

在64位计算机上,短字符串中可以包含22个字符。sizeof(string)是24。

一个主要的设计目标是最小化sizeof(string),同时使内部缓冲区尽可能大。基本原理是加快搬迁的建设和搬迁任务。越大sizeof,在移动构造或移动分配期间必须移动的单词越多。

长格式至少需要3个字来存储数据指针,大小和容量。因此,我将缩写形式限制为相同的3个单词。有人建议4字sizeof可能会有更好的性能。我尚未测试该设计选择。

_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 ++的供应商更改此标志。


17
不确定libc ++和Facebook Folly之间是否存在许可证兼容性,但是FBstring通过将大小更改为剩余容量来设法存储一个额外的字符(即23),以便它可以作为23个字符的短字符串的空终止符。
TemplateRex

20
@TemplateRex:那很聪明。但是,如果采用libc ++,则它将要求libc ++放弃我喜欢的其他有关其std :: string的特性:默认构造string为全0位。这使得默认构造超级有效。而且,如果您愿意违反规则,有时甚至可以自由。例如,您可以calloc记忆并只声明它充满了默认构造的字符串。
Howard Hinnant 2014年

6
啊,0-init确实不错!顺便说一句,FBstring有2个标志位,指示短,中和大字符串。它使用SSO来存储最多23个字符的字符串,然后使用malloc-ed内存区域来存储最多254个字符的字符串,除此之外,它们还执行COW(我知道在C ++ 11中不再合法)。
TemplateRex

为什么不能在intS中存储大小和容量,以便在64位体系结构上仅将类压缩为16个字节?
phuclv

@LưuVĩnhPhúc:我想在64位上允许大于2Gb的字符串。公认的代价是更大sizeof。但是同时,内部缓冲区char从14变为22,这是一个非常不错的好处。
Howard Hinnant

21

libc中++实现有点复杂,我会忽略它的替代性设计,并假设小端计算机:

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(aka)优化的一对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

老实说,我不太确定这是Standard C ++(我知道其中的初始子序列规定,union但不知道它如何与匿名联合和别名混在一起),但是允许标准库利用定义的实现反正行为。


感谢您提供详细的答案!我唯一缺少的是__min_cap对不同体系结构的评估结果,我不确定sizeof()返回的内容以及混叠将如何影响它。
ValarDohaeris 2014年

1
@ValarDohaeris是实现的定义。通常,3 * the size of one pointer在这种情况下,您会期望在32位拱形上为12个八位位组,在64位拱形上为24个八位位组。
justin 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.