Виртуальные вызовы функций немного дороже, чем обычные вызовы.Помимо фактического выполнения вызова, среда выполнения должна сначала определить, какую функцию вызывать, что часто приводит к:
- нахождению указателя v-таблицы и, через него, достижению v-таблицы
- Нахождение указателя функции в v-таблице и выполнение через него вызова
По сравнению с прямым вызовом, где адрес функции известен заранее (и жестко закодирован с помощьюсимвол), это приводит к небольшим накладным расходам.Хорошим компиляторам удается сделать его всего на 10-15% медленнее, чем обычный вызов, что обычно незначительно, если у функции есть какой-либо символ.* Девиртуализация функциональных вызовов, как правило, является низко висящим плодом.Например, см. В C ++ 03:
struct Base { virtual ~Base(); };
struct Derived: Base { virtual ~Derived(); };
void foo() {
Derived d; (void)d;
}
Clang получает:
define void @foo()() {
; Allocate and initialize `d`
%d = alloca i8**, align 8
%tmpcast = bitcast i8*** %d to %struct.Derived*
store i8** getelementptr inbounds ([4 x i8*]* @vtable for Derived, i64 0, i64 2), i8*** %d, align 8
; Call `d`'s destructor
call void @Derived::~Derived()(%struct.Derived* %tmpcast)
ret void
}
Как видите, компилятор был достаточно умен, чтобы определить, что d
являетсяDerived
, тогда нет необходимости переносить накладные расходы на виртуальный вызов.
Фактически, это оптимизировало бы следующую функцию так же приятно:
void bar() {
Base* b = new Derived();
delete b;
}
Однако в некоторых ситуацияхкомпилятор не может прийти к такому выводу:
Derived* newDerived();
void deleteDerived(Derived* d) { delete d; }
Здесь можно ожидать (наивно), что вызов deleteDerived(newDerived());
приведет к тому же коду, что и раньше.Однако дело обстоит не так:
define void @foobar()() {
%1 = tail call %struct.Derived* @newDerived()()
%2 = icmp eq %struct.Derived* %1, null
br i1 %2, label %_Z13deleteDerivedP7Derived.exit, label %3
; <label>:3 ; preds = %0
%4 = bitcast %struct.Derived* %1 to void (%struct.Derived*)***
%5 = load void (%struct.Derived*)*** %4, align 8
%6 = getelementptr inbounds void (%struct.Derived*)** %5, i64 1
%7 = load void (%struct.Derived*)** %6, align 8
tail call void %7(%struct.Derived* %1)
br label %_Z13deleteDerivedP7Derived.exit
_Z13deleteDerivedP7Derived.exit: ; preds = %3, %0
ret void
}
Соглашение может предписывать, что newDerived
возвращает Derived
, но компилятор не может сделать такое предположение: а что, если он вернул что-то еще производное?И, таким образом, вы видите все уродливые механизмы, задействованные в извлечении указателя v-таблицы, выборе соответствующей записи в таблице и, наконец, выполнении вызова.
Если, однако, мы добавим final
, то мыдать компилятору гарантию, что это не может быть чем-то еще:
define void @deleteDerived2(Derived2*)(%struct.Derived2* %d) {
%1 = icmp eq %struct.Derived2* %d, null
br i1 %1, label %4, label %2
; <label>:2 ; preds = %0
%3 = bitcast i8* %1 to %struct.Derived2*
tail call void @Derived2::~Derived2()(%struct.Derived2* %3)
br label %4
; <label>:4 ; preds = %2, %0
ret void
}
Короче говоря: final
позволяет компилятору избежать накладных расходов на виртуальные вызовы для соответствующих функций в ситуациях, когда обнаружение этого невозможно.