一、前言
在 C++ 开发中,std::string 是最常用的数据结构之一。表面上它只是一个“字符串类”,但其底层实现却暗藏玄机。理解这些原理,不仅有助于写出更高效的代码,还能避免一些隐蔽的性能陷阱。本文我们聚焦一个核心机制:小字符串优化(Small String Optimization, SSO)。
二、小字符串优化(SSO)的核心思想
C++ 标准并没有强制 std::string 的具体实现,但主流库(如 GCC 的 libstdc++ 和 LLVM 的 libc++)都实现了 小字符串优化(SSO)。
SSO 的基本思路是:
- 当字符串较短时,直接存在 std::string 对象本身内部的 buffer 中,而不去堆上分配内存。
- 只有超过一定阈值(常见是 15 个字节)时,才会使用堆内存。
以 GCC 11.5(libstdc++,x86-64) 为例,std::string 的i源码为:
84 template<typename _CharT, typename _Traits, typename _Alloc>
85 class basic_string
86 {
// ......省略
158 struct _Alloc_hider : allocator_type // TODO check __is_final
159 {
160 #if __cplusplus < 201103L
161 _Alloc_hider(pointer __dat, const _Alloc& __a = _Alloc())
162 : allocator_type(__a), _M_p(__dat) { }
163 #else
164 _Alloc_hider(pointer __dat, const _Alloc& __a)
165 : allocator_type(__a), _M_p(__dat) { }
166
167 _Alloc_hider(pointer __dat, _Alloc&& __a = _Alloc())
168 : allocator_type(std::move(__a)), _M_p(__dat) { }
169 #endif
170
171 pointer _M_p; // The actual data.
172 };
173
174 _Alloc_hider _M_dataplus;
175 size_type _M_string_length;
176
177 enum { _S_local_capacity = 15 / sizeof(_CharT) };
178
179 union
180 {
181 _CharT _M_local_buf[_S_local_capacity + 1];
182 size_type _M_allocated_capacity;
183 };
// ......省略
3106 };
这解释了为什么在 GCC 11.5 下 std::string 的大小是 32 字节,以及为什么 SSO 容量正好是 15。
也就是说:
- 当字符串长度 ≤ 15 时,内容直接存放在 _M_local_buf 中;
- 当长度 ≥ 16 时,才会分配堆内存并把 _M_p 指向它。
三、验证 SSO 的小实验
下面这段代码可以验证 std::string 的大小,以及 15 和 16 字节时的分配差异:
// test.cpp
// g++ test.cpp -o test
#include <string>
#include <iostream>
const char* str_15 = "abcdefghijklmno"; // length is 15
const char* str_16 = "abcdefghijklmnoj"; // length is 16
void print_usage()
{
std::cerr << "Usage: ./test [15|16]" << std::endl;
}
int main(int argc, char** argv) {
if (argc < 2) {
print_usage();
return -1;
}
const char* str = nullptr;
int arg = std::stoi(argv[1]);
if (arg == 15) {
str = str_15;
} else if (arg == 16) {
str = str_16;
} else {
print_usage();
return -1;
}
std::cout << "sizeof(std::string) = " << sizeof(std::string) << std::endl;
// 无限循环创建临时std::string对象
while (true) {
std::string s(str);
}
return 0;
}
接下来,创建perf监控事件用于监控进程调用malloc的情况:
[root@instance-bguv65e0 string_sso]# perf probe /lib64/libc.so.6 malloc
Added new event:
probe_libc:malloc (on malloc in /lib64/libc.so.6)
You can now use it in all perf tools, such as:
perf record -e probe_libc:malloc -aR sleep 1
分别运行string为15字节和16字节两种场景,并使用perf监控此进程调用malloc的情况,输出示例(GCC 11.5, x86-64):
[root@instance-bguv65e0 string_sso]# ./test 15 &
[1] 4162216
[root@instance-bguv65e0 string_sso]# sizeof(std::string) = 32
[root@instance-bguv65e0 string_sso]# perf stat -e probe_libc:malloc -t 4162216 -- sleep 5
Performance counter stats for thread id '4162216':
0 probe_libc:malloc
5.001849405 seconds time elapsed
[root@instance-bguv65e0 string_sso]#
[root@instance-bguv65e0 string_sso]# ./test 16 &
[1] 4162321
[root@instance-bguv65e0 string_sso]# sizeof(std::string) = 32
[root@instance-bguv65e0 string_sso]# perf stat -e probe_libc:malloc -t 4162321 -- sleep 5
Performance counter stats for thread id '4162321':
1,604,918 probe_libc:malloc
5.001863361 seconds time elapsed
[root@instance-bguv65e0 string_sso]#
分析运行结果可知,15字节std::string 没有触发堆分配,而16字节std::string触发了堆分配,从而直观印证了 SSO 的阈值就是15。
四、性能上的巨大意义
为什么这对性能优化如此重要?
- 避免动态内存分配
堆分配 (malloc/free) 是昂贵的操作,可能导致锁竞争、缓存失效。
短字符串(如日志标签、JSON 字段名、临时拼接 key 等)非常常见,避免堆分配能显著提升性能。 - 提升缓存命中率
小字符串直接存在对象内部,连续存放在栈或数组中,局部性更好。
CPU cache 命中率提升,内存访问速度加快。 - 减少碎片化
大量短字符串如果频繁分配/释放,会造成堆碎片化。SSO 避免了这种问题。
五、结语
std::string 的小字符串优化(SSO)是一个看似微不足道却极其重要的性能特性。
它让我们在日常编程中无须担心小字符串的性能问题,大多数短小字符串操作几乎是零开销的。
写高性能 C++ 时,我们可以放心大胆地使用 std::string,而不必像早期那样到处用 const char* 或手写缓冲区来规避堆分配。
📬 欢迎关注VX公众号“Hankin-Liu的技术研究室”,持续分享信创、软件性能测试、调优、编程技巧、软件调试技巧相关内容,输出有价值、有沉淀的技术干货。