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

时间:2014-02-11 06:01:51

标签: c++ string optimization c++-standard-library libc++

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内存布局。

2 个答案:

答案 0 :(得分:105)

libc ++ basic_string旨在在所有体系结构上都有sizeof个3个字,其中sizeof(word) == sizeof(void*)。您已正确解剖了长/短标志,以及缩写形式的大小字段。

  

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

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

  • 1位进入长/短旗。
  • 7位大小。
  • 假设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中的初始子序列规定,但不知道它是如何与匿名联合和别名一起抛出的),而是一个标准库允许无论如何都要利用实现定义的行为。