libc++ 和 libstdc++ 中 basic_string 的 SSO 比较

参考

https://joellaity.com/2020/01/31/string.html

std::stringstd::basic_string<> 的一个特化,该类的 value_type 是 char。本文虽然是讨论 std::basic_string<> 的实现方式,但是为了方便,假设 value_typechar、假设目标平台是 64 位,讨论时也将把 std::basic_string<>std::string 互用。

libstdc++

libstdc++ 是 gcc 所用的标准库实现,也是 clang 在不提供 -stdlib=... 选项下的默认标准库实现。在 64 位环境下,libstdc++ 中的 std::string 占用 32 个字节。

Tip

std::vector<> 的大小(std::vector<bool> 有特化,所以是例外,在 测试 看到 libstdc++ 和 libc++ 中 std::vector<bool> 的大小分别是 40 和 24 字节)是 24 字节,因为它也需要 capacity + size + pointer 三元组。

std::string 把 capacity + size + pointer 三元组的 capacity 改成了 buffer 和 capacity 的联合体,其中 buffer 大小为 16 字节,可以容纳除去 \0 的 15 个字符。当 size 小于等于临界值时,联合体存储的就是 buffer,否则是 capacity。

pointer + union{buffer(16B), capacity} + size

Note

这里给定的表示方式不代表各数据域的实际排列顺序,下同。

在这样的实现下,std::string 最终的大小是 32 字节,SSO 容量为 15 个字节。访问 capacity 需要先确定是否是本地存储,有条件判断的过程;访问 size 和 pointer 则是直接获取,当字符串本地存储时 pointer 指向字符串内的 buffer。

libc++

libc++ 中 std::string 有 long mode 和 short mode 两种模式,因此有一个模式标记位。下面讨论小端,大端有另外的适配逻辑,但是总体上类似。

在 short mode(即 SSO 模式)下,布局是 size + buffer,并且 size 的最不重要位(least significant bit)作为模式标记为,存储 0。模式标志位的存在使得存储字符串实际大小时,需要将大小值左移 1 位,因此只有 7 位可用(最大 127,已经超过 std::string 大小,够用了)。

size(1B) + buffer(23B)
      ^ flag bit

在 long mode 下,使用 capacity + size + pointer 的经典布局。capacity 的最不重要字节的最不重要位作为模式标记,存储 1。此时,真实的容量是 capacity 去掉末尾 0,这同时意味着每次动态分配内存时,申请的字节数得是 2 的倍数,这是很容易办到的(数学上最多浪费 2-1=1 个字节,实际上字符串是倍增申请,所以完全不会浪费字节)。

capacity + size + pointer
       ^ flag bit

为了保证模式位在两种模式下的存储位置相同,大小端需要做不同的处理。我们希望这个模式位处在 capacity 的最不重要字节,又希望 short mode 中 buffer 连续,因此在大端平台上,long mode 的 capacity 排在开头,short mode 的 size 是字符串中地址最小的字节;在小端平台上,long mode 的 capacity 排在数据域的结尾,short mode 的 size 是字符串中地址最大的字节。

因此,libc++ 中 std::string 占用 24 个字节,同时 SSO 字符数是 22(去掉 \0)。在访问 size、capacity、pointer 前都需要确定 long mode 还是 short mode,多了 1 次判断过程。

Note

如果 value_type 的大小大于 1,那么在大端模式下可能需要一些填充以保证 1 字节的 size 和 capacity 的最不重要字节对齐。

验证 SSO 边界

libstdc++

#include <string>
// clang++ main.cc -o main && valgrind ./main
int main () {
    std::string s(15, 'a'); // 修改成 16 再试一次
}

libc++

#include <string>
// clang++ main.cc -o main -stdlib=libc++ && valgrind ./main
int main () {
    std::string s(22, 'a'); // 修改成 23 再试一次
}

Note

不要只看内存申请的总次数,要看修改字符串长度之后内存申请次数的差值。在我的测试中,libstdc++ 还有别的地方会申请内存,因此内存申请次数比 libc++ 多了一次(这个情况仅在我本机的环境下测试过,不保证以后版本更新了仍然保持)。

Tip

2024 年 4 月 14 日:有个更好的方法是创建空字符串,然后用 s.capacity() 直接查看容量,之前一时没有想到。