У питанні C ++ про оптимізацію та стиль коду кілька відповідей посилаються на "SSO" у контексті оптимізації копій std::string
. Що означає ССО у цьому контексті?
Очевидно, що не "єдиний знак увімкнено". "Спільна оптимізація рядків", можливо?
У питанні C ++ про оптимізацію та стиль коду кілька відповідей посилаються на "SSO" у контексті оптимізації копій std::string
. Що означає ССО у цьому контексті?
Очевидно, що не "єдиний знак увімкнено". "Спільна оптимізація рядків", можливо?
Відповіді:
Операції з автоматичними змінними ("зі стека", що є змінними, які ви створюєте без виклику 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;
};
};
Я припускаю, що більшість реалізацій виглядають приблизно так.
std::string const &
, потрапляння на дані є єдиною непрямою пам'яттю, оскільки дані зберігаються в місці посилання. Якби не було невеликої оптимізації рядків, для доступу до даних потрібні були би дві непрямі пам'яті (спочатку для завантаження посилання на рядок і зчитування її вмісту, потім другого для зчитування вмісту вказівника даних у рядку).
SSO - це абревіатура для "Small String Optimization" - техніки, коли невеликі рядки вбудовуються в тіло класу рядків, а не використовують окремо виділений буфер.
Як уже було пояснено в інших відповідях, 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 залежить від реалізації платформи.
std::string
реалізується", а інша запитує "що означає SSO", ви повинні бути абсолютно божевільним, щоб вважати їх одним і тим же питанням