Немножко про оптимизации и C++ 1/2 [это база].
0. SSO -- small/short string optimization.
Если мы посмотрим на
sizeof(std::string), почти наверняка (без упоминания специфических архитектур и старых компиляторов) мы увидим 32 (что больше, чем ожидаемые 24 при стандартной схеме с указателем на данные и двумя чиселками size и capacity). Примерно потому что на самом деле строка выглядит так:template <typename T>
struct basic_string {
char* begin_;
size_t size_;
union {
size_t capacity_;
char sso_buffer[16];
};
};Что позволяет хранить значения маленьких строк сразу на стеке -> не тратить время на аллокацию/деаллокацию. Но, конечно же, есть и минусы. Например, кроме того, что ваш
sizeof может стать больше (может и не стать, всё зависит от размера вашего буфера), вы ещё делаете move-операции более дорогими. У bfilipek есть небольшая статья про то, как узнать размер этого буфера с помощью
constexpr/constinit. А тут на so можно посмотреть какой-то домашний бенчмарк с пруфом, что это действительно что-то ускоряет. sso -- частный случай small object/buffer optimization.
Такой подход также применяется в
std::function. Затрекать можно следующим кодом:auto f = [i = 0]() { std::cout << &i << ' '; };
std::function<void()> alpha = f;
alpha();
std::function<void()> beta = std::move(alpha);
beta();Если данные хранятся на куче, то при муве мы просто переложим указатель на область памяти из
alpha в beta. Если же данные будут хранится на стеке, то придётся перекладывать сам объект и адреса в выводе будут разные (gb). Ещё sbo используют в различных реализациях
small_vector. Причём если в случае std::string/std::function вы храните либо на стеке, либо на куче, то тут иногда делают вперемешку: могут часть оставить в буффере, а часть вынести на кучу; ну или на кучу всё, чтобы данные лежали в непрерывном куске памяти. 1. Empty base [class] optimization (ebo/ebco).
Имеем такой код:
struct A {};
struct B : A {};Как известно, в C++
sizeof пустого объекта -- 1 байт. Т.е. у B размер должен быть хотя бы 2 байта. Но конечно же это не так. Он всё ещё 1 байт, потому что в дело вступает ebco. В случае нескольких родителей каждый компилятор делает что хочет. В msvc, например, оптимизация применяется только к последнему родителю (остальные занимают по байту). Clang и GCC же применяют ко всем.
Предположим, мы хотим использовать кастомный делитер с
std::unique_ptr. Как сделать так, чтобы хранение делитера не занимало памяти? В случае, если делитер это какой-то stateless класс-функтор, от него можно отнаследоваться. Тогда в дело вступает ebco, что позволяет не занимать лишний байт, но при этом использовать operator(). Обычно это делают с помощью compressed_pair (реализовано это примерно разбором случаев на компиляции, когда можно наследоваться от типа, а когда нет; если нельзя, то тип просто хранится как член класса). Так что, если вам нужен кастомный делитер, лучше делать его без состояния. Рядом с ebco обычно говорят ещё про атрибут
[[no_unique_address]] (C++20). Когда вы помечаете какой-то из членов класса подобным атрибутом, вы говорите, что этот объект может не иметь уникального адреса в памяти и потенциально перекрываться с другими членами класса. Так с C++20 вы можете реализовать compressed_pair гораздо проще:template <typename T, typename U>
struct compressed_pair {
[[no_unique_address]] T _val1;
[[no_unique_address]] U _val2;
};Компилятор сам посмотрит, правда ли типы пустые, и, если да, не будет выделять для них лишнюю память. Подобное можно использовать вместе с stateless аллокаторами, предикатами, хеш-функциями и любыми другими подобными объектами-функторами.
Про другие атрибуты писал тут: https://t.me/thisnotes/218.