Quant developer HFT course · Module 3: Containers and memory
Small buffer optimisation
The question
Why does std::string("ESZ6") not allocate, but a 40-character string does? What is sizeof(std::string)?
The trick
Many objects are usually small. Most strings in a trading system are symbols, venue codes and short tags; most callbacks capture a pointer or two; most vectors of legs hold one or two legs. Allocating heap memory for each is wasteful when the contents would fit in the object itself.
Small buffer optimisation reserves space inside the object for small contents and uses the heap only when they do not fit. The object is a little larger; most instances never allocate.
std::string
All three major standard libraries implement it for strings, with different layouts:
| Library | sizeof(std::string) on 64-bit | Characters stored inline |
|---|---|---|
| libstdc++ (GCC) | 32 bytes | up to 15 |
| libc++ (Clang) | 24 bytes | up to 22 |
| MSVC | 32 bytes | up to 15 |
So std::string("ESZ6") stores four characters and a terminator inside the object; a 40-character string allocates. The boundary is a property of the library, not of the language, which is a good thing to say when asked.
Some consequences:
- Moving a short string copies its characters. There is no heap buffer to steal, so a move of an inline string is a small copy. It is still cheap.
- Pointers into a short string move with it.
s.data()points inside the object, so after the string is moved or copied elsewhere, a pointer taken earlier refers to the old object's buffer. capacity()of an empty string is not zero. It reports the inline capacity, 15 or 22.
Other places it appears
std::functionstores small callables inline, as covered in Module 1. The buffer is small, typically room for two pointers in libstdc++.std::anydoes the same for small values.- Small vectors. Libraries such as Boost, Folly and LLVM provide
small_vector<T, N>: a vector with room for N elements inside it that moves to the heap only beyond N. The standard does not have one; C++26 addsstd::inplace_vector<T, N>, which has fixed capacity and never allocates at all.
boost::container::small_vector<Leg, 4> legs; // up to 4 legs without touching the heap
std::inplace_vector<Leg, 4> fixed; // C++26: at most 4, never allocatesCosts
- Size. Every instance pays for the inline buffer, used or not. A vector of a million
std::strings with libstdc++ is 32 MB before any characters are stored. - A branch. Operations check whether the contents are inline or on the heap. This is cheap and predictable for uniform data.
- Larger moves. Moving an object with inline contents copies the buffer rather than a pointer.
Doing it yourself
For hot-path data with a known bound, the simplest version is a fixed buffer with no heap fallback at all: a symbol stored as char sym[8] and compared as one 64-bit integer, or a fixed-capacity array of legs. The fallback branch disappears, the size is fixed, and running past the bound becomes a validation error at the edge of the system.
struct Symbol {
char c[8] = {};
bool operator==(const Symbol& o) const { return std::memcmp(c, o.c, 8) == 0; } // one 8-byte compare
};
static_assert(sizeof(Symbol) == 8);Check yourself
Question 1 of 3
With libstdc++, which of these constructions allocates on the heap?
libstdc++ stores up to 15 characters inline. Sixteen characters plus a terminator need 17 bytes, beyond the 16-byte local buffer.
Question 2 of 3
What does moving a short string stored inline cost?
There is no heap buffer to transfer, so the bytes are copied. That is cheap, and it also means pointers into the old string's buffer do not follow the contents.
Question 3 of 3
What is the main cost of an inline buffer?
The space is reserved in each object. For large arrays of mostly empty objects that can be significant memory traffic.
In the interview
Say your own answer out loud first, then open the one we would give.
What is small string optimisation?
The string object keeps a small buffer inside itself and stores short strings there, allocating only for longer ones. In libstdc++ the object is 32 bytes with room for 15 characters; libc++ is 24 bytes with room for 22. Most symbols and tags never touch the heap.
Where else is small buffer optimisation used?
std::function and std::any store small callables and values inline, and libraries provide small_vector with inline room for a few elements. C++26's inplace_vector goes further with a fixed capacity and no heap at all.
What are its downsides?
Every object carries the buffer, so it is bigger; operations branch on inline versus heap; and moving inline contents copies them, which also means pointers into the old object's buffer do not follow the data.