Виртуальные вызовы функций немного дороже, чем обычные вызовы. В дополнение к фактическому выполнению вызова среда выполнения должна сначала определить, какую функцию вызывать, что часто приводит к:
- Нахождение указателя v-таблицы и через него доступ к v-таблице
- Нахождение указателя функции в v-таблице и выполнение вызова через него
По сравнению с прямым вызовом, когда адрес функции известен заранее (и жестко закодирован с помощью символа), это приводит к небольшим накладным расходам. Хорошим компиляторам удается сделать его всего на 10-15% медленнее, чем обычный вызов, что обычно несущественно, если у функции есть мясо.
Оптимизатор компилятора по-прежнему стремится избежать всевозможных накладных расходов, а девиртуализация вызовов функций, как правило, является легкой задачей. Например, см. в C++03:
struct Base { virtual ~Base(); };
struct Derived: Base { virtual ~Derived(); };
void foo() {
Derived d; (void)d;
}
Кланг получает:
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
позволяет компилятору избежать накладных расходов на виртуальные вызовы соответствующих функций в ситуациях, когда их обнаружение невозможно.
person
Matthieu M.
schedule
24.09.2011
override
иfinal
состоит в том, чтобы позволить компилятору помешать пользователю облажаться. Вы должны использовать их, чтобы остановить себя и других от неправильных поступков. То, что компилятор может или не может сделать что-то быстрее, на самом деле не имеет значения, потому что вы должны всегда использовать их там, где это уместно. - person Nicol Bolas   schedule 24.09.2011