Яка механіка оптимізації коротких струн у libc ++?


102

Ця відповідь дає хороший огляд високого рівня оптимізації коротких рядків (SSO). Однак я хотів би більш детально дізнатися, як це працює на практиці, зокрема в реалізації libc ++:

  • Наскільки короткою повинна бути рядок, щоб мати право на отримання SSO? Це залежить від цільової архітектури?

  • Як реалізація розрізняє короткі та довгі рядки під час доступу до рядкових даних? Це так просто, m_size <= 16чи це прапор, який є частиною іншої змінної члена? (Я думаю, що m_sizeце може бути використано для зберігання рядкових даних).

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

Ось кілька спостережень після перегляду джерела :

libc ++ може бути складено з двома дещо різними схемами пам'яті для класу string, цим регулюється _LIBCPP_ALTERNATE_STRING_LAYOUTпрапор. Обидва макети також розрізняють машини малої ендіанської та великої ендіанських, що залишає нам усього 4 різних варіанти. Я буду припускати "нормальну" компоновку і малоефективне в наступному.

Якщо припустити, що size_typeце 4 байти і value_typeце 1 байт, ось так виглядатимуть перші 4 байти рядка в пам'яті:

// short string: (s)ize and 3 bytes of char (d)ata
sssssss0;dddddddd;dddddddd;dddddddd
       ^- is_long = 0

// long string: (c)apacity
ccccccc1;cccccccc;cccccccc;cccccccc
       ^- is_long = 1

Оскільки розмір короткої рядка знаходиться у верхніх 7 бітах, її потрібно змістити під час доступу до неї:

size_type __get_short_size() const {
    return __r_.first().__s.__size_ >> 1;
}

Аналогічно, геттер і сетер для ємності довгої струни використовує __long_maskдля обходу навколоis_long біта.

Я все ще шукаю відповідь на своє перше запитання, тобто яке значення має __min_cap вміст коротких рядків для різних архітектур?

Інші стандартні реалізації бібліотеки

Ця відповідь дає хороший огляд std::stringмакетів пам'яті в інших стандартних реалізаціях бібліотеки.


libc ++ є відкритим кодом, його stringзаголовок ви можете знайти тут , я зараз його перевіряю :)
Матьє М.


@Matthieu M .: Я вже бачив, що раніше, на жаль, це дуже великий файл, дякую за допомогу в його перевірці.
ValarDohaeris

@Ali: Я натрапив на це, гуляючи. Однак у цьому дописі в блозі прямо написано, що це лише ілюстрація SSO, а не дуже оптимізований варіант, який би використовувався на практиці.
ValarDohaeris

Відповіді:


120

Libc ++ basic_stringрозроблений так, щоб мати sizeof3 слова на всіх архітектурах, де sizeof(word) == sizeof(void*). Ви правильно розсікали довгий / короткий прапор та поле розміру в короткій формі.

яке значення має значення __min_cap, ємність коротких рядків для різних архітектур?

У короткій формі є 3 слова для роботи:

  • 1 біт йде до довгого / короткого прапора.
  • 7 біт іде в розмір.
  • Якщо припустити char, 1 байт переходить до остаточного нуля (libc ++ завжди буде зберігати проміжну нуль за даними).

Це залишає 3 слова мінус 2 байти для зберігання короткого рядка (тобто найбільшого capacity()без виділення).

На 32-розрядній машині 10 символів поміститься в коротку струну. sizeof (рядок) - 12.

На 64-бітній машині 22 символи помістяться в коротку струну. sizeof (рядок) - 24.

Головною метою дизайну було мінімізувати sizeof(string), роблячи при цьому внутрішній буфер максимально великим. Обґрунтуванням є швидкість побудови руху та призначення переміщення. Чим більше, тимsizeof , тим більше слів вам доведеться перенести під час побудови ходу або призначення.

Для довгої форми потрібні мінімум 3 слова для зберігання вказівника даних, розміру та ємності. Тому я обмежив коротку форму тими ж 3-ма словами. Висловлюється думка, що розмір у 4 слова може мати кращі показники. Я не перевіряв цей вибір дизайну.

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

Існує прапор конфігурації, _LIBCPP_ABI_ALTERNATE_STRING_LAYOUTякий називається, який переставляє члени даних таким чином, що "довгий макет" змінюється з:

struct __long
{
    size_type __cap_;
    size_type __size_;
    pointer   __data_;
};

до:

struct __long
{
    pointer   __data_;
    size_type __size_;
    size_type __cap_;
};

Мотивацією цієї зміни є віра в те, що ставити __data_ перше місце матиме певні переваги у виконанні завдяки кращому вирівнюванню. Була зроблена спроба виміряти переваги продуктивності, і важко було виміряти. Це не погіршить ефективність роботи, а може зробити її трохи кращою.

Прапором слід користуватися обережно. Це інший ABI, і якщо випадково змішаний з libc ++, std::stringскладеним з іншим налаштуванням _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT, створить помилки часу виконання.

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


17
Не впевнений, чи існує сумісність ліцензій між libc ++ та Facebook Folly, але FBstring вдається зберігати додатковий знак (тобто 23), змінюючи розмір до залишкової ємності , щоб він міг виконувати подвійний обов'язок як нульовий термінатор для короткого рядка з 23 знаків .
TemplateRex

20
@TemplateRex: Це розумно. Однак якщо libc ++ приймає, йому потрібно буде libc ++ відмовитись від однієї іншої характеристики, яка мені подобається в її std :: string: По замовчуванню побудовано stringвсі 0 біт. Це робить конструкцію за замовчуванням надто ефективною. І якщо ви готові дотримуватися правил, іноді навіть безкоштовно. Наприклад, ви могли б callocпам’яті та просто оголосити, що вона повна створених за замовчуванням рядків.
Говард Хінант

6
Ах, 0-init справді приємно! BTW, FBstring має 2 біти прапора, що вказують на короткі, проміжні та великі рядки. Він використовує SSO для рядків до 23 знаків, а потім використовує область з обмеженою пам’яттю для рядків розміром до 254 символів і більше того, що вони роблять COW (більше не є законним в C ++ 11, я знаю).
TemplateRex

Чому розмір та ємність не можна зберігати в ints, щоб клас міг бути упакований лише до 16 байт у 64-бітних архітектурах?
phuclv

@ LưuVĩnhPhúc: Я ​​хотів дозволити рядки, що перевищують 2 Гб, на 64-бітних. Вартість, правда, більша sizeof. Але в той же час внутрішній буфер для charпереходу від 14 до 22, що є досить хорошою перевагою.
Говард Хінант

21

Реалізація libc ++ трохи складна, я проігнорую її альтернативний дизайн та припустимо, що маленький комп'ютер із ендіанією:

template <...>
class basic_string {
/* many many things */

    struct __long
    {
        size_type __cap_;
        size_type __size_;
        pointer   __data_;
    };

    enum {__short_mask = 0x01};
    enum {__long_mask  = 0x1ul};

    enum {__min_cap = (sizeof(__long) - 1)/sizeof(value_type) > 2 ?
                      (sizeof(__long) - 1)/sizeof(value_type) : 2};

    struct __short
    {
        union
        {
            unsigned char __size_;
            value_type __lx;
        };
        value_type __data_[__min_cap];
    };

    union __ulx{__long __lx; __short __lxx;};

    enum {__n_words = sizeof(__ulx) / sizeof(size_type)};

    struct __raw
    {
        size_type __words[__n_words];
    };

    struct __rep
    {
        union
        {
            __long  __l;
            __short __s;
            __raw   __r;
        };
    };

    __compressed_pair<__rep, allocator_type> __r_;
}; // basic_string

Примітка: __compressed_pairпо суті є пара, оптимізована для оптимізації порожньої бази , ака template <T1, T2> struct __compressed_pair: T1, T2 {};; для всіх намірів і цілей ви можете вважати це звичайною парою. Його значення якраз і полягає в тому, що std::allocatorвін без громадянства і, таким чином, порожній.

Гаразд, це досить сиро, тому давайте перевіримо механіку! Внутрішньо багато функцій викличуть, __get_pointer()який сам викликає, __is_longщоб визначити, чи використовує рядок __longабо __shortпредставлення:

bool __is_long() const _NOEXCEPT
    { return bool(__r_.first().__s.__size_ & __short_mask); }

// __r_.first() -> __rep const&
//     .__s     -> __short const&
//     .__size_ -> unsigned char

Якщо чесно, я не надто впевнений, що це стандарт C ++ (я знаю, що початкове положення про підпорядкування, unionале не знаю, як воно з'єднується з анонімним об'єднанням і псевдонімом. поведінка все одно.


Дякую за цю детальну відповідь! Єдиний твір, який мені не вистачає, - це те, __min_capщо оцінювали б різні архітектури, я не впевнений, що sizeof()повернеться і як на нього вплине збиток.
ValarDohaeris

1
@ValarDohaeris визначено реалізацію. як правило, ви б очікували, що 3 * the size of one pointerв цьому випадку буде 12 октетів на 32-бітовій арці та 24 на 64-бітовій арці.
Джастін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.