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


155

У питанні C ++ про оптимізацію та стиль коду кілька відповідей посилаються на "SSO" у контексті оптимізації копій std::string. Що означає ССО у цьому контексті?

Очевидно, що не "єдиний знак увімкнено". "Спільна оптимізація рядків", можливо?


57
Це лише дублікат так само, як "те, що є 2 + 2", є дублікатом "що є результатом 200/50". Відповідь однакова. Питання зовсім інше. "Закрити як дублікат" призначений для використання, коли кілька людей задають одне і те саме * питання. Коли одна людина запитує "як std::stringреалізується", а інша запитує "що означає SSO", ви повинні бути абсолютно божевільним, щоб вважати їх одним і тим же питанням
jalf

1
@jalf: Якщо є існуючий Q + A, який точно охоплює сферу цього питання, я вважаю це дублікатом (я не кажу, що ОП повинен був це шукати сам, просто що будь-яка відповідь тут буде висвітлювати грунт, це вже висвітлювались.)
Олівер Чарльзворт,

47
Ви фактично говорите ОП, що "ваше запитання неправильне. Але вам потрібно було знати відповідь, щоб знати, що ви мали б запитати". Хороший спосіб відключити людей. Це також робить непотрібним важко знайти потрібну інформацію. Якщо люди не ставлять запитань (а закриття фактично говорить "це питання не слід було задавати"), тоді люди не могли б відповісти, щоб отримати відповідь на це питання
jalf

7
@jalf: Зовсім не. ІМО, "голосування про закриття" не означає "поганого питання". Я використовую для цього downvotes. Я вважаю це дублікатом у тому сенсі, що всі безліч питань (i = i ++ тощо), відповідь яких - «невизначена поведінка» - це дублікати один одного. З іншого приводу, чому ніхто не відповів на питання, якщо це не дублікат?
Олівер Чарльворт,

5
@jalf: Я погоджуюся з Олі, питання не є дублікатом, але відповідь була б, тому перенаправлення на інше питання, де відповіді вже лежать, здається доречним. Питання, закриті як дублікати, не зникають, натомість вони виступають вказівниками на інше запитання, де лежить відповідь. Наступна людина, яка шукає SSO, опиниться тут, слідкує за перенаправленням і знайде її відповідь.
Матьє М.

Відповіді:


213

Передумови / огляд

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

SSO - це оптимізація коротких / малих рядків. std::stringЗазвичай зберігає рядок як покажчик на вільний магазин ( «купа»), що дає аналогічні характеристики , як якщо б ви були на заклик new char [size]. Це запобігає переповненню стека для дуже великих рядків, але це може бути повільніше, особливо при копіюванні. В якості оптимізації багато реалізацій std::stringстворюють невеликий автоматичний масив, щось подібне char [20]. Якщо у вас є рядок, що має 20 символів або менше (з огляду на цей приклад, фактичний розмір змінюється), він зберігає її безпосередньо в цьому масиві. Це уникає необхідності newвзагалі телефонувати , що дещо прискорює роботу.

Редагувати:

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

Деталі реалізації

Як мінімум, 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;
    };
};

Я припускаю, що більшість реалізацій виглядають приблизно так.


7
Ось гарне пояснення деяких реальних реалізацій: stackoverflow.com/a/28003328/203044
BillT

Чи справді SSO практичний, коли більшість розробників передає std :: string, використовуючи const посилання?
Гупта

1
SSO має дві переваги, крім того, щоб скопіювати копіювання. Перший полягає в тому, що якщо розмір рядка відповідає маленькому розміру буфера, вам не потрібно виділяти початкову конструкцію. Друга полягає в тому, що коли функція приймає a std::string const &, потрапляння на дані є єдиною непрямою пам'яттю, оскільки дані зберігаються в місці посилання. Якби не було невеликої оптимізації рядків, для доступу до даних потрібні були би дві непрямі пам'яті (спочатку для завантаження посилання на рядок і зчитування її вмісту, потім другого для зчитування вмісту вказівника даних у рядку).
Девід Стоун

34

SSO - це абревіатура для "Small String Optimization" - техніки, коли невеликі рядки вбудовуються в тіло класу рядків, а не використовують окремо виділений буфер.


15

Як уже було пояснено в інших відповідях, 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 на Ubuntu

Macbook Pro Орієнтир SSO на Macbook Pro

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.