Девиртуализация доступа к полям виртуально наследованных классов



В предыдущей статье мы разобрались с механизмом доступа к полям виртуально наследованных классов. Так же нам удалось установить, что он сопряжен с накладными расходами.



Причина нам известна — это разыменование указателей для доступа к виртуальной таблице смещений. Мы вынуждены к ней обращаться, т.к. в силу отсутствия каких-либо ограничений, смещение может быть любым. В общем случае, мы просто не можем гарантировать, что его виртуальная база находится именно там. Возникает вопрос, можно ли добавить какие-то ограничения, которые позволят вычислить смещение на этапе компиляции? Можем ли мы на это как-то повлиять?



То, что мы хотим сделать, называется девиртуализацией. Мы хотим выполнить оптимизацию, которая позволит получить прямой доступ к полям класса, минуя таблицу виртуальных смещений. Мы действительно можем это сделать — достаточно лишь понять суть проблемы: компилятору неизвестно, будет ли текущий класс наследован кем-то другим из других единиц компиляции. Новые наследники добавят какое-то количество байт под свои поля и смещение изменится (но в текущей единице компиляции нам это будет неизвестно). Получается, нам достаточно явно ограничить возможность наследования от конкретного класса!



Тут следует сделать оговорку, что стандарт C++ никак не регламентирует реализацию оптимизаций. Следовательно, это необходимо дополнительно проверять для вашего компилятора. И теперь вы будете знать, что именно! 😉 Мы проверяли на компиляторах gcc, llvm clang, icc/icx под x86-64.



Как вы уже догадались, запретить наследование можно с помощью идентификатора со специальным значением final:

struct Child final : P2, P1 {};



Этого достаточно, чтобы гарантировать отсутствие наследников класса Child в других единицах трансляции. Следовательно, при работе с данным наследником смещение может быть вычислено на этапе компиляции и использовано без обращения к виртуальной таблице:

auto *pointer = new Child();



// Direct cast Child* -> Base*

auto *base = static_cast<Base*>(pointer);



Важно отметить, что приведение типа должно быть прямым от финального класса к виртуальной базе, без приведения к промежуточным наследникам! Иначе мы опять будем вынуждены обращаться к таблице виртуальных смещений.



Я видоизменил пример из предыдущей статьи и получил вот такого монстра бенчмарка. Тут появился шаблонный класс inheritance_cast, который в зависимости от булевой константы вызывает либо одну, либо другую реализацию для приведения типа (мотивация выше). Это нужно исключительно для моего бенчмарка. Писать такие вспомогательные классы вам нет никакого смысла, ведь вы должны знать, с каким классом вы работаете.



Новые результаты демонстрируют, что теперь скорость доступа стала сопоставимой с невиртуально наследованным классом. Следовательно, можно сделать вывод, что девиртуализация доступа позволяет сократить лишние действия!



Нам так же следует поговорить о девиртуализации, когда разберем работу динамического полиморфизма. Всем удачи!



#cppcore #compiler