Какова механика оптимизации коротких строк в libС++?

Этот ответ дает хороший общий обзор оптимизации коротких строк (SSO). Однако я хотел бы узнать более подробно, как это работает на практике, особенно в реализации libc++:

  • Насколько короткой должна быть строка, чтобы претендовать на SSO? Это зависит от целевой архитектуры?

  • Как реализация различает короткие и длинные строки при доступе к строковым данным? Это так же просто, как m_size <= 16 или это флаг, который является частью какой-то другой переменной-члена? (Я представьте, что m_size или его часть также может быть использована для хранения строковые данные).

Я задал этот вопрос специально для libc++, потому что я знаю, что он использует SSO, это даже упоминается на libc++ домашней странице.

Вот некоторые наблюдения после просмотра источника:

libc++ может быть скомпилирован с двумя немного разными макетами памяти для строкового класса, это регулируется флагом _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 в других реализациях стандартной библиотеки.

Ответ 1

libС++ basic_string предназначен для sizeof 3 слов на всех архитектурах, где sizeof(word) == sizeof(void*). Вы правильно расчленили длинный/короткий флаг и поле размера в короткой форме.

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

В короткой форме есть 3 слова для работы с:

  • 1 бит переходит к длинному/короткому флагом.
  • 7 бит идет на размер.
  • Предполагая char, 1 байт переходит в конечный нуль (libС++ всегда сохраняет конечный нуль за данными).

Это оставляет 3 слова минус 2 байта для хранения короткой строки (т.е. наибольшей capacity() без выделения).

На 32-битной машине 10 коротких строк будут вписываться в короткую строку. sizeof (string) - 12.

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

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

В длинной форме требуется минимум 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, и если случайно смешанный с libС++ std::string, скомпилированный с другим параметром _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT, создаст ошибки времени выполнения.

Я рекомендую, чтобы этот флаг был изменен только поставщиком libС++.

Ответ 2

libС++-реализация немного сложна, я проигнорирую ее альтернативный дизайн и предположим маленький конечный компьютер:

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

Честно говоря, я не уверен, что это Standard С++ (я знаю начальное положение подпоследовательности в union, но не знаю, как он соединяется с анонимным объединением и сглаживанием вместе), но стандартная библиотека разрешена чтобы в любом случае воспользоваться определением, определяемым реализацией.