Теория девиртуализации 😶
Виртуальные функции обычно работают через vtable. Если метод виртуальный, то вместо вызова точно известного метода, в рантайме вычисляется адрес метода, который зависит от динамического типа объекта.
Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный 😐
Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память.
void CallDo(TDerived& obj) {
obj.Do(); // будет ли девиртуализация?
}
Компилятор считает, что можно девиртуализовать вызов в таких случаях:1️⃣ Метод класса помечен как
final.Смысл в том, что даже в случае работы с объектами
TDerived*/TDerived& (которые могут указывать на наследника TDerived) нужный метод будет одним и тем же, как его определил класс TDerived.struct TDerived : IBase {
void Do() final override; // слово `final` тут
};
2️⃣ Класс является финальным. Смысл в том, что указатель на этот класс не будет указывать на какого-то наследника, который что-то мог бы переопределить, потому что у такого класса просто не может быть наследников.struct TDerived final : IBase { // слово final тут
void Do() override;
};
Однако есть еще одно условие, когда класс считается финальным - если у него финальный деструктор 😁 Этот прикол я обнаружил в исходнике Clang.struct TDerived : IBase {
~TDerived() final = default; // слово final тут
void Do() override;
};
3️⃣ Мы работаем с объектом класса, а не с указателем на класс. В этом случае точный класс объекта известен на этапе компиляции.TDerived derived;
derived.Do(); // это же TDerived, инфа 100%
4️⃣ Объект является prvalue. В C++ есть укуренная классификация объектов, где prvalue (pure value) это грубо говоря выражение которое создает новый объект. Смысл в том, что в этом случае тоже известен точный класс объекта на этапе компиляции.TDerived MakeDerived(); // просто функция
// ...
MakeDerived().Do(); // здесь будет девиртуализация
TDerived{}.Do(); // здесь тоже девиртуализация
На этом всё! Эта оптимизация логичная и скучная, потому что никаких чудес ожидать не приходится. Смысл в том, чтобы доказать что TDerived/TDerived&/TDerived* указывает именно на объект TDerived, а не на какой-то его потомок.В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редких
(1) работа с
TDerived* вместо IBase*;(2) Класс
TDerived или нужный метод финальный (не помню когда в последний раз писал final).Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид.
Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено.
Например, в Apple macOS👩💻 есть Kext (Kernel Extension) - расширения ядра, запускающие то или иное несовместимое с оригинальным маком оборудование. Особенность этих Kext в том, что они могут в рантайме менять vtable, поэтому нельзя делать оптимизации, которые обходят обращение к vtable. В Clang есть флаг -fapple-kext для такой настройки.
А в "обычных" окружениях vtable лежат в секциях наподобии
.rodata. Эта секция защищена на уровне операционной системы - программа обычно сразу падает при попытке сделать туда какую-нибудь запись в рантайме.