C++学习笔记 47 小字符串优化 SSO

小字符串优化 SSO

能允许速度慢的话 就不要用C++了 减少字符串的使用 就是减少内存分配

STL对于小到一定程度的字符串 可以只分配一小块基于栈的缓冲区 而不是堆分配的 所以如果你有一个非常小的字符串 就不用考虑const char*或者试图微观管理 优化你的代码 因为STL本来就不会做堆分配

为了防止堆分配 可能你使用const char* name = "Miku"; 但其实这里并没有堆分配 这符合C++的小字符串 只存储在一个静态分配的缓冲区 不会使用堆内存

右键代码中的std::string 查看定义 到达这一行 using string = basic_string<char, char_traits, allocator>; 所以string其实是basic_string的别名 右键basic_string 转到定义 就到达了basic_string类

如何阅读STL源码

我们就以std::string为例 学习如何阅读STL源码 重点关注小字符串优化机制

右键头文件#include 的string 转到文档 就到达了头文件 也就几百行 说明这其中是没有具体实现的 在代码中任何一处右键 - 大纲显示 - 折叠到定义 就可以看到这里是一些全局函数 比如getline stoi to_string

回到文件开头 可以看到include了一些头文件 约定x前缀表示核心容器实现 右键转到文档 这份文件有5000多行 继续右键折叠到定义

ctrl+F 打开匹配大小写 搜索class string 没有找到 搜索string 看到了非常多的basic_string_view basic_string 直到我们看到了一行_EXPORT_STD using string = basic_string<char, char_traits, allocator>; 于是我们知道 string就是basic_string的别名 于是继续搜索class basic_string 发现这是一个2000多行的类 就是我们要找的核心实现 很多STL的核心实现都是以basic_命名

_EXPORT_STD template <class _Elem, class _Traits = char_traits<_Elem>, class _Alloc = allocator<_Elem>>
class basic_string {

}

这是一个模板类 拿到任何一个类 我们都需要查看

核心成员变量
构造函数 析构函数
内存管理策略
常用操作
可以先按ctrl+K ctrl+K 为这个basic_string类添加一个书签

往下看 找到一个不接收任何参数的构造函数

basic_string() noexcept(is_nothrow_default_constructible_v<_Alty>) : _Mypair(_Zero_then_variadic_args_t{}) {
        _Mypair._Myval2._Alloc_proxy(_GET_PROXY_ALLOCATOR(_Alty, _Getal()));
        _Tidy_init();
    }

不禁要问 _Mypair是什么 右键_Mypair 速览定义

_Compressed_pair<_Alty, _Scary_val> _Mypair;
用同样的方式查看_Compressed_pair类 注释里写store a pair of values, deriving from empty first 在本例中它存储了一对_Alty _Scary_val 我们对_Scary_val右键速览定义 发现基本上就是_String_val的别名

_String_val是一个类 是实现小字符串优化的核心

class _String_val : public _Container_base {
public:
    using value_type      = typename _Val_types::value_type;
    using size_type       = typename _Val_types::size_type;
    using difference_type = typename _Val_types::difference_type;
    using pointer         = typename _Val_types::pointer;
    using const_pointer   = typename _Val_types::const_pointer;
    using reference       = value_type&;
    using const_reference = const value_type&;

    _CONSTEXPR20 _String_val() noexcept : _Bx() {}

    // length of internal buffer, [1, 16] (NB: used by the debugger visualizer)
    static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type);
    // roundup mask for allocated buffers, [0, 15]
    static constexpr size_type _Alloc_mask = sizeof(value_type) <= 1 ? 15
                                           : sizeof(value_type) <= 2 ? 7
                                           : sizeof(value_type) <= 4 ? 3
                                           : sizeof(value_type) <= 8 ? 1
                                                                     : 0;
    // capacity in small mode
    static constexpr size_type _Small_string_capacity = _BUF_SIZE - 1;

    _NODISCARD _CONSTEXPR20 value_type* _Myptr() noexcept {
        value_type* _Result = _Bx._Buf;
        if (_Large_mode_engaged()) {
            _Result = _Unfancy(_Bx._Ptr);
        }

        return _Result;
    }

    _NODISCARD _CONSTEXPR20 const value_type* _Myptr() const noexcept {
        const value_type* _Result = _Bx._Buf;
        if (_Large_mode_engaged()) {
            _Result = _Unfancy(_Bx._Ptr);
        }

        return _Result;
    }

    _NODISCARD _CONSTEXPR20 bool _Large_mode_engaged() const noexcept {
        return _Myres > _Small_string_capacity;
    }

    _CONSTEXPR20 void _Activate_SSO_buffer() noexcept {
        // start the lifetime of the array elements
#if _HAS_CXX20
        if (_STD is_constant_evaluated()) {
            for (size_type _Idx = 0; _Idx < _BUF_SIZE; ++_Idx) {
                _Bx._Buf[_Idx] = value_type();
            }
        }
#endif // _HAS_CXX20
    }

    _CONSTEXPR20 void _Check_offset(const size_type _Off) const {
        // checks whether _Off is in the bounds of [0, size()]
        if (_Mysize < _Off) {
            _Xran();
        }
    }

    _CONSTEXPR20 void _Check_offset_exclusive(const size_type _Off) const {
        // checks whether _Off is in the bounds of [0, size())
        if (_Mysize <= _Off) {
            _Xran();
        }
    }

    [[noreturn]] static void _Xran() {
        _Xout_of_range("invalid string position");
    }

    _NODISCARD _CONSTEXPR20 size_type _Clamp_suffix_size(const size_type _Off, const size_type _Size) const noexcept {
        // trims _Size to the longest it can be assuming a string at/after _Off
        return (_STD min)(_Size, _Mysize - _Off);
    }

    union _Bxty { // storage for small buffer or pointer to larger one
        // This constructor previously initialized _Ptr. Don't rely on the new behavior without
        // renaming `_String_val` (and fixing the visualizer).
        _CONSTEXPR20 _Bxty() noexcept : _Buf() {} // user-provided, for fancy pointers
        _CONSTEXPR20 ~_Bxty() noexcept {} // user-provided, for fancy pointers

        value_type _Buf[_BUF_SIZE];
        pointer _Ptr;
        char _Alias[_BUF_SIZE]; // TRANSITION, ABI: _Alias is preserved for binary compatibility (especially /clr)
    };
    _Bxty _Bx;

    // invariant: _Myres >= _Mysize, and _Myres >= _Small_string_capacity (after string's construction)
    // neither _Mysize nor _Myres takes account of the extra null terminator
    size_type _Mysize = 0; // current length of string (size)
    size_type _Myres  = 0; // current storage reserved for string (capacity)
};

我们将逐行分析

_String_val类的构造函数是_String_val() noexcept : _Bx() {}

在类的后半段可以看到 _Bx是一个_Bxty

union _Bxty {
    _CONSTEXPR20 _Bxty() noexcept : _Buf() {} // 构造函数 初始化_Buf数组 即小缓冲区
    _CONSTEXPR20 ~_Bxty() noexcept {} // 析构函数

    value_type _Buf[_BUF_SIZE]; // 小缓冲区数组 类型为value_type 长度为_BUF_SIZE 用于存储较短字符串内容 实现小字符串优化
    pointer _Ptr; // 指针 用于当字符串较长时存储指向堆上分配的大缓冲区的指针
    char _Alias[_BUF_SIZE]; // 用于二进制兼容 暂时不用管
};
_Bxty _Bx;

这是一个联合体 官方有注释说 存储小的buffer或者指向更大buffer的指针 使用联合体可以让同一块内存可以用不同方式解释 实现小字符串优化

所以这个_String_val类的构造函数_String_val() noexcept : _Bx() {} 就是创建了一个空的名为_Bx的_Bxty类型联合体

之前我们没有提到联合体的构造函数 其实联合体是可以有构造函数的 这个_Bxty类型联合体的构造函数_Bxty() noexcept : _Buf() {} 只是创建了一个_Buf[_BUF_SIZE]数组 实际上也等同于创建了一个指针_Ptr 但联合体不能同时激活多个成员 于是在构造时选择了初始化_Buf 那么就是默认为小字符串 而在使用_Ptr(堆分配)前需要先通过placement new激活 我们目前还不知道_Buf数组的_BUF_SIZE 所以在创建之前需要设置好_BUF_SIZE 而且我们也不知道联合体里的_Ptr在哪里激活

于是回到类的开头 首先解决_BUF_SIZE的问题 可以通过双击 将_BUF_SIZE高亮 迅速定位到这里

static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type);

static constexpr size_type _Alloc_mask = sizeof(value_type) <= 1 ? 15
                                       : sizeof(value_type) <= 2 ? 7
                                       : sizeof(value_type) <= 4 ? 3
                                       : sizeof(value_type) <= 8 ? 1
                                                                 : 0;

static constexpr size_type _Small_string_capacity = _BUF_SIZE - 1; 
  1. 第一句 static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type);
  • sizeof(value_type) 是这个类型的一个字符占用的字节数
  • 16 / sizeof(value_type) 是16字节空间里能放下几个value_type类型的字符
    <1就是一个都放不进去 那就取1 否则就取实际能放进去的数目
    一个都放不进去却仍然取1 是为了前面那个联合体_Bxty成员_Buf[_BUF_SIZE]至少有一个元素 类型安全
  1. 由于只要sizeof(value_type)>=16 _BUF_SIZE就是1 所以第三句static constexpr size_type _Small_string_capacity = _BUF_SIZE - 1; 这时小字符串的容量就是0 实际上就会走长字符串分支 采用指针存储

  2. 第二句那么复杂的长句 是用于内存分配时对齐 减少碎片

通过这几个操作 我们得到了_BUF_SIZE _Alloc_mask _Small_string_capacity

现在来解决_Ptr激活的问题 实际上当联合体包含平凡类型时 并不需要显式地使用placement new 可以直接通过赋值来切换激活成员 这是因为平凡类型没有复杂的构造或者析构 而_Buf数组和_Ptr指针都是平凡类型

_NODISCARD _CONSTEXPR20 value_type* _Myptr() noexcept {
    value_type* _Result = _Bx._Buf;
    if (_Large_mode_engaged()) {
        _Result = _Unfancy(_Bx._Ptr);
    }

    return _Result;
}

_NODISCARD _CONSTEXPR20 const value_type* _Myptr() const noexcept {
    const value_type* _Result = _Bx._Buf;
    if (_Large_mode_engaged()) {
        _Result = _Unfancy(_Bx._Ptr);
    }

    return _Result;
}

_NODISCARD _CONSTEXPR20 bool _Large_mode_engaged() const noexcept {
    return _Myres > _Small_string_capacity;
}

value_type* _Result = _Bx._Buf;
_Result是一个指针 _Bx是一个联合体 这个联合体要么是小字符串直接存 要么就是指向长字符串的指针 _Bx._Buf就是那个小字符串

而if (_Large_mode_engaged()) 也就是_Myres > _Small_string_capacity _Myres表示当前字符串的容量 那么_Result就指向_Bx._Ptr _Unfancy通常是去掉可能存在的指针包装

_Myptr()所做的事就是 字符串长度超过16 就会切换为指针 没超过就直接存

我们现在就要回到basic_string 看看哪里调用了_Myptr()

在basic_string类中 构造函数之后 就可以看到一些常用的方法 比如重写的操作符

_CONSTEXPR20 basic_string& operator=(const _Elem _Ch) { // assign {_Ch, _Elem()}
    _ASAN_STRING_MODIFY(*this, _Mypair._Myval2._Mysize, 1);
    _Mypair._Myval2._Mysize = 1;
    _Elem* const _Ptr       = _Mypair._Myval2._Myptr();
    _Traits::assign(_Ptr[0], _Ch);
    _Traits::assign(_Ptr[1], _Elem());
    return *this;
}

_Mypair是_Compressed_pair<_Alty, _Scary_val> 那么_Myval2就是_Scary_val 也即_String_val 而_Myptr()是_String_val的成员函数 所以_Elem* const _Ptr = _Mypair._Myval2._Myptr();就是获取字符串数据的指针 无论是直接存储的小字符串 还是堆分配的字符串

只要大于等于16个字节就会发生分配 可以重写operator new 在release模式下进行测试

posted @ 2026-01-02 11:04  超轶绝尘  阅读(11)  评论(0)    收藏  举报