Значение аббревиатуры SSO в контексте std::string

В вопрос С++ об оптимизации и стиле кода несколько ответов относятся к "SSO" в контексте оптимизации копий std::string. Что означает SSO в этом контексте?

Понятно, что не "один знак". "Оптимизация разделяемой строки", возможно?

Ответ 1

Фон/Обзор

Операции с автоматическими переменными ( "из стека", которые вы создаете без вызова malloc/new), обычно намного быстрее, чем операции с бесплатным хранилищем ( "куча", которые являются переменными, которые создаются с помощью new). Тем не менее, размер автоматических массивов фиксируется во время компиляции, но размер массивов из бесплатного хранилища не является. Более того, размер стека ограничен (как правило, несколько MiB), тогда как свободное хранилище ограничено только вашей системной памятью.

SSO - это оптимизация коротких/малых строк. A std::string обычно сохраняет строку как указатель на свободное хранилище ( "куча" ), что дает аналогичные характеристики производительности, как если бы вы звонили new char [size]. Это предотвращает переполнение стека для очень больших строк, но оно может быть медленнее, особенно при копировании. В качестве оптимизации многие реализации std::string создают небольшой автоматический массив, что-то вроде char [20]. Если у вас есть строка, длина которой составляет 20 символов или меньше (в данном примере изменяется фактический размер), он хранит ее непосредственно в этом массиве. Это позволяет избежать вызова new вообще, что немного ускоряет работу.

EDIT:

Я не ожидал, что этот ответ будет настолько популярен, но, поскольку это так, позвольте мне дать более реалистичную реализацию, с оговоркой, что я никогда не читал никакой реализации SSO "в дикой природе".

Сведения о реализации

Как минимум, a std::string необходимо сохранить следующую информацию:

  • Размер
  • Емкость
  • Местоположение данных

Размер может быть сохранен как std::string::size_type или как указатель на конец. Единственное различие заключается в том, хотите ли вы вычесть два указателя, когда пользователь вызывает size или добавляет size_type к указателю, когда пользователь вызывает end. Емкость также может быть сохранена в любом случае.

Вы не платите за то, что не используете.

Сначала рассмотрим наивную реализацию, основанную на том, что я изложил выше:

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;
};

Для 64-разрядной системы это обычно означает, что std::string имеет 24 байта "служебных" данных для каждой строки, плюс еще 16 для буфера SSO (16 выбрано здесь вместо 20 из-за требований дополнения). Было бы бесполезно хранить эти три элемента данных плюс локальный массив символов, как в моем упрощенном примере. Если m_size <= 16, тогда я поместил все данные в m_sso, поэтому я уже знаю емкость, и мне не нужен указатель на данные. Если m_size > 16, то мне не нужно m_sso. Нет абсолютно никакого перекрытия, где мне все они нужны. Более разумное решение, которое не тратило бы пространство, выглядело бы чем-то более похожим на это (непроверенные, только примерные цели):

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;
    };
};

Я бы предположил, что большинство реализаций больше похожи на это.

Ответ 2

SSO - это аббревиатура для "Оптимизация небольших строк", метод, в котором маленькие строки встроены в тело строкового класса, а не используя отдельно выделенный буфер.

Ответ 3

Как уже объяснялось в других ответах, SSO означает Оптимизация малых/коротких строк. Мотивация этой оптимизации является неопровержимым доказательством того, что приложения в целом обрабатывают намного более короткие строки, чем более длинные строки.

Как объяснил Дэвид Стоун в своем ответе выше, класс std::string использует внутренний буфер для хранения содержимого до заданной длины, что исключает необходимость динамического выделения памяти. Это делает код более эффективным и быстрым.

Этот другой связанный ответ ясно показывает, что размер внутреннего буфера зависит от реализации std::string, которая варьируется от платформы к платформе (см. результаты тестов ниже).

Ориентиры

Вот небольшая программа, которая тестирует операцию копирования множества строк одинаковой длины. Начинается печать времени для копирования 10 миллионов строк длиной = 1. Затем он повторяется со строками длины = 2. Он продолжается до тех пор, пока длина не станет 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;
}

Если вы хотите запустить эту программу, вы должны сделать это как ./a.out > /dev/null, чтобы время печати строк не учитывалось. Значимые числа печатаются в stderr, поэтому они отображаются в консоли.

Я создал диаграммы с выводом из моих машин MacBook и Ubuntu. Обратите внимание, что существует огромный скачок во времени для копирования строк, когда длина достигает заданной точки. Это тот момент, когда строки больше не помещаются во внутренний буфер и распределение памяти должно быть использовано.

Также обратите внимание, что на машине linux скачок происходит, когда длина строки достигает 16. На macbook переход происходит, когда длина достигает 23. Это подтверждает, что SSO зависит от реализации платформы.

Ubuntu SSO benchmark on Ubuntu

Macbook Pro SSO benchmark on Macbook Pro