1. Introduction
In C++ development, std::string is one of the most commonly used data structures. On the surface, it is merely a “string class,” but its underlying implementation contains hidden intricacies. Understanding these principles not only helps in writing more efficient code but also avoids some subtle performance pitfalls. In this article, we focus on a core mechanism: Small String Optimization (SSO).
2. Core Idea of Small String Optimization (SSO)
The C++ standard does not mandate a specific implementation of std::string, but mainstream libraries (such as GCC’s libstdc++ and LLVM’s libc++) implement Small String Optimization (SSO). The basic idea of SSO is:
-
When the string is short, it is stored directly in the internal buffer of the std::string object, without allocating memory on the heap.
-
Only when it exceeds a certain threshold (commonly 15 bytes) will heap memory be used.
For example, in GCC 11.5 (libstdc++, x86-64), the source code of std::string is as follows:
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 };
This explains why the size of std::string is 32 bytes in GCC 11.5, and why the SSO capacity is exactly 15.
In other words:
-
When the string length ≤ 15, the content is stored directly in _M_local_buf;
-
When the length ≥ 16, heap memory is allocated and _M_p points to it.
3. A Small Experiment to Validate SSO
The following code can validate the size of std::string and the allocation differences at 15 and 16 bytes:
// 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;
// Infinite loop creating temporary std::string objects while (true) { std::string s(str); } return 0;}
Next, create perf monitoring events to monitor the process’s malloc calls:
[root@instance-bguv65e0 string_sso]# perf probe /lib64/libc.so.6 mallocAdded 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
Run the string for both 15-byte and 16-byte scenarios, and use perf to monitor the malloc calls of this process. Example output (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]#
From the analysis of the running results, it can be seen that the 15-byte std::string did not trigger heap allocation, while the 16-byte std::string did trigger heap allocation, intuitively confirming that the SSO threshold is indeed 15.
4. The Significant Impact on Performance
Why is this so important for performance optimization?
1. Avoiding dynamic memory allocation Heap allocation (malloc/free) is an expensive operation that can lead to lock contention and cache misses. Short strings (such as log tags, JSON field names, temporary concatenation keys, etc.) are very common, and avoiding heap allocation can significantly improve performance.
2. Improving cache hit rates
Small strings are stored directly within the object, continuously in the stack or array, leading to better locality. This increases CPU cache hit rates and speeds up memory access.
3. Reducing fragmentation Frequent allocation/release of many short strings can cause heap fragmentation. SSO avoids this issue.
5. Conclusion
The Small String Optimization (SSO) of std::string is a seemingly trivial yet extremely important performance feature.It allows us to program without worrying about the performance issues of small strings, as most operations on short strings are virtually zero-cost.When writing high-performance C++, we can confidently use std::string without having to resort to using const char* or manually managing buffers to avoid heap allocation as we did in the past.📬 Feel free to follow for continuous sharing of content related to software performance testing, optimization, programming techniques, and debugging skills, providing valuable and substantial technical insights.