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

What are the mechanics of short string optimization in libc++?

提问人:ValarDohaeris 提问时间:2/11/2014 最后编辑:ValarDohaeris 更新时间:2/8/2019 访问量:59750

问:

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

  • 字符串必须有多短才能获得 SSO 的资格? 这是否取决于目标架构?

  • 实现如何区分短期和长期 访问字符串数据时的字符串?它是像其他成员变量一样简单,还是作为其他成员变量一部分的标志?(我 想象一下,它的一部分也可能用于存储 字符串数据)。m_size <= 16m_size

我专门针对 libc++ 问了这个问题,因为我知道它使用 SSO,这甚至在 libc++ 主页上都有提及。

以下是查看来源后的一些观察结果:

libc++ 可以使用字符串类的两种略有不同的内存布局进行编译,这由 flag 控制。这两种布局还区分了 little-endian 和 big-endian 机器,这给我们总共留下了 4 种不同的变体。在下文中,我将假设“正常”布局和 little-endian。_LIBCPP_ALTERNATE_STRING_LAYOUT

进一步假设这是 4 个字节,即 1 个字节,这就是字符串的前 4 个字节在内存中的样子:size_typevalue_type

// 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_maskis_long

我仍在寻找第一个问题的答案,即短字符串的容量对于不同的架构会有什么价值?__min_cap

其他标准库实现

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

C 字符串 优化 C++-标准库 libc++

评论

0赞 Matthieu M. 2/11/2014
libc++ 是开源的,你可以在这里找到它的标题,我现在正在检查它:)string
0赞 Ali 2/11/2014
您可能对小字符串优化和移动操作感兴趣
1赞 ValarDohaeris 2/12/2014
@Matthieu M.:我以前见过,不幸的是,这是一个非常大的文件,感谢您的帮助。
1赞 ValarDohaeris 2/12/2014
@Ali:我在谷歌上搜索时偶然发现了这一点。但是,这篇博文明确表示,它只是 SSO 的一个说明,而不是在实践中使用的高度优化的变体。

答:

24赞 Matthieu M. 2/11/2014 #1

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_pairtemplate <T1, T2> struct __compressed_pair: T1, T2 {};std::allocator

好吧,这是相当原始的,所以让我们检查一下机制!在内部,许多函数将调用 which 本身调用以确定字符串是否使用 or 表示形式:__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

评论

0赞 ValarDohaeris 2/12/2014
感谢您的详细回答!我唯一缺少的是针对不同架构的评估,我不确定会返回什么以及它如何受到混叠的影响。__min_capsizeof()
1赞 justin 2/12/2014
@ValarDohaeris它的实现定义。通常,在这种情况下,您会期望在 32 位架构上为 12 个八位字节,在 64 位架构上为 24 个八位字节。3 * the size of one pointer
151赞 Howard Hinnant 2/12/2014 #2

libc++ 被设计为在所有架构上都有 3 个词,其中 .您已经正确地剖析了长/空标志,以及简写形式中的大小字段。basic_stringsizeofsizeof(word) == sizeof(void*)

__min_cap,即短字符串的容量,对不同的架构有什么价值?

在简短的形式中,有 3 个词可以使用:

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

这留下了 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,如果不小心与使用不同设置编译的 libc++ 混合,将产生运行时错误。std::string_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

我建议仅由 libc++ 供应商更改此标志。

评论

25赞 TemplateRex 2/12/2014
不确定 libc++ 和 Facebook Folly 之间是否存在许可证兼容性,但 FBstring 设法通过将大小更改为剩余容量来存储额外的字符(即 23),以便它可以作为 23 个字符的短字符串的 null 终止符执行双重任务。
27赞 Howard Hinnant 2/12/2014
@TemplateRex:这很聪明。但是,如果 libc++ 采用,它将要求 libc++ 放弃我喜欢其 std::string 的另一个特征:默认构造为所有 0 位。这使得默认构造非常高效。如果你愿意改变规则,有时甚至是免费的。例如,您可以内存并声明它充满了默认构造的字符串。stringcalloc
7赞 TemplateRex 2/12/2014
啊,0-init 确实不错!顺便说一句,FBstring 有 2 个标志位,表示短字符串、中间字符串和大字符串。它对最多 23 个字符的字符串使用 SSO,然后对最多 254 个字符的字符串使用 malloc-ed 内存区域,除此之外,它们执行 COW(我知道,在 C++11 中不再合法)。
0赞 phuclv 11/5/2016
为什么不能将大小和容量存储在 s 中,以便在 64 位体系结构上将类打包为仅 16 个字节?int
1赞 Howard Hinnant 11/5/2016
@LưuVĩnhPhúc:我想允许在 64 位上使用大于 2Gb 的字符串。诚然,成本更大.但与此同时,内部缓冲区从 14 增加到 22,这是一个相当不错的好处。sizeofchar