Meaning of acronym SSO in the context of std::string

C++StringOptimization

C++ Problem Overview


In a C++ question about optimization and code style, several answers referred to "SSO" in the context of optimizing copies of std::string. What does SSO mean in that context?

Clearly not "single sign on". "Shared string optimization", perhaps?

C++ Solutions


Solution 1 - C++

Background / Overview

Operations on automatic variables ("from the stack", which are variables that you create without calling malloc / new) are generally much faster than those involving the free store ("the heap", which are variables that are created using new). However, the size of automatic arrays is fixed at compile time, but the size of arrays from the free store is not. Moreover, the stack size is limited (typically a few MiB), whereas the free store is only limited by your system's memory.

SSO is the Short / Small String Optimization. A std::string typically stores the string as a pointer to the free store ("the heap"), which gives similar performance characteristics as if you were to call new char [size]. This prevents a stack overflow for very large strings, but it can be slower, especially with copy operations. As an optimization, many implementations of std::string create a small automatic array, something like char [20]. If you have a string that is 20 characters or smaller (given this example, the actual size varies), it stores it directly in that array. This avoids the need to call new at all, which speeds things up a bit.

EDIT:

I wasn't expecting this answer to be quite so popular, but since it is, let me give a more realistic implementation, with the caveat that I've never actually read any implementation of SSO "in the wild".

Implementation details

At the minimum, a std::string needs to store the following information:

  • The size
  • The capacity
  • The location of the data

The size could be stored as a std::string::size_type or as a pointer to the end. The only difference is whether you want to have to subtract two pointers when the user calls size or add a size_type to a pointer when the user calls end. The capacity can be stored either way as well.

You don't pay for what you don't use.

First, consider the naive implementation based on what I outlined above:

class string {
public:
	// all 83 member functions
private:
	std::unique_ptr<char[]> m_data;
	size_type m_size;
	size_type m_capacity;
	std::array<char, 16> m_sso;
};

For a 64-bit system, that generally means that std::string has 24 bytes of 'overhead' per string, plus another 16 for the SSO buffer (16 chosen here instead of 20 due to padding requirements). It wouldn't really make sense to store those three data members plus a local array of characters, as in my simplified example. If m_size <= 16, then I will put all of the data in m_sso, so I already know the capacity and I don't need the pointer to the data. If m_size > 16, then I don't need m_sso. There is absolutely no overlap where I need all of them. A smarter solution that wastes no space would look something a little more like this (untested, example purposes only):

class string {
public:
	// all 83 member functions
private:
	size_type m_size;
	union {
		class {
			// This is probably better designed as an array-like class
			std::unique_ptr<char[]> m_data;
			size_type m_capacity;
		} m_large;
		std::array<char, sizeof(m_large)> m_small;
	};
};

I'd assume that most implementations look more like this.

Solution 2 - C++

SSO is the abbreviation for "Small String Optimization", a technique where small strings are embedded in the body of the string class rather than using a separately allocated buffer.

Solution 3 - C++

As already explained by the other answers, SSO means Small / Short String Optimization. The motivation behind this optimization is the undeniable evidence that applications in general handle much more shorter strings than longer strings.

As explained by David Stone in his answer above, the std::string class uses an internal buffer to store contents up to a given length, and this eliminates the need to dynamically allocate memory. This makes the code more efficient and faster.

This other related answer clearly shows that the size of the internal buffer depends on the std::string implementation, which varies from platform to platform (see benchmark results below).

Benchmarks

Here is a small program that benchmarks the copy operation of lots of strings with the same length. It starts printing the time to copy 10 million strings with length = 1. Then it repeats with strings of length = 2. It keeps going until the length is 50.

#include <string>
#include <iostream>
#include <vector>
#include <chrono>

static const char CHARS[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static const int ARRAY_SIZE = sizeof(CHARS) - 1;

static const int BENCHMARK_SIZE = 10000000;
static const int MAX_STRING_LENGTH = 50;

using time_point = std::chrono::high_resolution_clock::time_point;

void benchmark(std::vector<std::string>& list) {
	std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();

	// force a copy of each string in the loop iteration
	for (const auto s : list) {
		std::cout << s;
	}

	std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
	const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
	std::cerr << list[0].length() << ',' << duration << '\n';
}

void addRandomString(std::vector<std::string>& list, const int length) {
	std::string s(length, 0);
	for (int i = 0; i < length; ++i) {
		s[i] = CHARS[rand() % ARRAY_SIZE];
	}
	list.push_back(s);
}

int main() {
	std::cerr << "length,time\n";

	for (int length = 1; length <= MAX_STRING_LENGTH; length++) {
		std::vector<std::string> list;
		for (int i = 0; i < BENCHMARK_SIZE; i++) {
			addRandomString(list, length);
		}
		benchmark(list);
	}

	return 0;
}

If you want to run this program, you should do it like ./a.out > /dev/null so that the time to print the strings isn't counted. The numbers that matter are printed to stderr, so they will show up in the console.

I have created charts with the output from my MacBook and Ubuntu machines. Note that there is a huge jump in the time to copy the strings when the length reaches a given point. That's the moment when strings don't fit in the internal buffer anymore and memory allocation has to be used.

Note also that on the linux machine, the jump happens when the length of the string reaches 16. On the macbook, the jump happens when the length reaches 23. This confirms that SSO depends on the platform implementation.

Ubuntu SSO benchmark on Ubuntu

Macbook Pro SSO benchmark on Macbook Pro

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRaedwaldView Question on Stackoverflow
Solution 1 - C++David StoneView Answer on Stackoverflow
Solution 2 - C++Mark RansomView Answer on Stackoverflow
Solution 3 - C++HugoTeixeiraView Answer on Stackoverflow