Память виртуально наследованных классов
Как было сказано в предыдущей статье, «просто так эти области памяти не могут быть объединены...». Возможность совместного использования памяти для общего родительского класса есть!
С помощью ключевого слова
Стандарт языка не регламентирует реализацию виртуального наследования и виртуальных методов. Большинство компиляторов придерживаются спецификации Itanium C++ ABI (в частности, GCC и LLVM Clang). Однако, различия всё равно могут быть. Нам важно получить именно понимание, какие могут быть нюансы и как они могут быть решены.
Давайте сразу посмотрим, как это будет представлено в памяти. Рассмотрим живой пример 5:
Мы тут же сталкиваемся с одной интересной проблемой. В рамках класса
Как может решаться эта проблема? В большинстве случаев, во все классы, которые используют виртуальное наследование, неявно добавляется виртуальный указатель на виртуальную таблицу смещений. Она генерируется компилятором и хранится в read-only памяти приложения. Размер класса, естественным образом, увеличивается сразу же на размер указателя для выбранной архитектуры:
При обращении к полям виртуально наследованного класса, будет выполняться дополнительная операция чтения виртуального указателя
Как вы догадываетесь, за такие фокусы приходится платить тактами процессора. Это подтверждают результаты бенчмарка. Действительно, доступ к памяти виртуально наследованных классов будет работать медленнее.
В следующей статье мы поговорим о возможном способе оптимизации скорости доступа к памяти виртуально наследованных классов.
#cppcore #compiler
Как было сказано в предыдущей статье, «просто так эти области памяти не могут быть объединены...». Возможность совместного использования памяти для общего родительского класса есть!
С помощью ключевого слова
virtual
, объявляется виртуально наследованный класс:struct Base {..};Таким образом мы сообщаем компилятору, что память родительского класса
struct P1 : virtual Base {..};
struct P2 : virtual Base {..};
struct Child : P2, P1 {..};
Base
будет использоваться совместно классами P2
и P1
, которые виртуально наследуются от него.Стандарт языка не регламентирует реализацию виртуального наследования и виртуальных методов. Большинство компиляторов придерживаются спецификации Itanium C++ ABI (в частности, GCC и LLVM Clang). Однако, различия всё равно могут быть. Нам важно получить именно понимание, какие могут быть нюансы и как они могут быть решены.
Давайте сразу посмотрим, как это будет представлено в памяти. Рассмотрим живой пример 5:
struct Base {..};Да, как и раньше при множественном наследовании, адресы
struct P1 : virtual Base {..};
struct P2 : virtual Base {..};
struct Child: P2, P1 {..};
Layout of Child:
0: [ Memory of struct P2 ] <- P2*, Child*
16: [ Memory of struct P1 ] <- P1*
32: [ Memory of struct Child ]
40: [ Memory of struct Base ] <- Child::Base*, P2::Base*, P1::Base*
P1
и P2
будут отличны друг от друга, но вот их родительский класс Base
теперь вынесен отдельно и существует в единственном исполнении.Мы тут же сталкиваемся с одной интересной проблемой. В рамках класса
Child
мы можем вычислить смещение до полей класса Base
. Но сам по себе класс Child
может быть далеко не единственным классом, который наследует P1
или P2
. Я хочу сказать, что мы не можем просто "запомнить" это смещение и использовать его для других классов. Нам так же непонятно, как вычислить это смещение, если мы работаем абстрагировано с классом P1
или P2
. Вдруг это самостоятельный объект, а может быть это родительский класс Child
или Child2
? Более того, сам Child
может быть унаследован другими классами, что добавит новые поля и изменит итоговое смещение. Вообще говоря, эта информация может даже меняться во время выполнения программы. Вот тут и начинается веселье!Как может решаться эта проблема? В большинстве случаев, во все классы, которые используют виртуальное наследование, неявно добавляется виртуальный указатель на виртуальную таблицу смещений. Она генерируется компилятором и хранится в read-only памяти приложения. Размер класса, естественным образом, увеличивается сразу же на размер указателя для выбранной архитектуры:
struct Base {..};
struct P1 : virtual Base {..};
Layout of P1:
0 | (P1 vtable pointer) // + 8 байт
8 | uint64_t data_of_P1
16 | struct Base (virtual base)
16 | uint64_t data_of_Base
При обращении к полям виртуально наследованного класса, будет выполняться дополнительная операция чтения виртуального указателя
vtable pointer
для доступа к виртуальной таблице смещений. И вуаля, теперь мы уже знаем, где у нас лежит наша Base
.Как вы догадываетесь, за такие фокусы приходится платить тактами процессора. Это подтверждают результаты бенчмарка. Действительно, доступ к памяти виртуально наследованных классов будет работать медленнее.
В следующей статье мы поговорим о возможном способе оптимизации скорости доступа к памяти виртуально наследованных классов.
#cppcore #compiler