对函数的虚拟调用比正常调用的成本略高。除了实际执行调用之外,运行时必须首先确定调用哪个函数,这通常会导致:
- 定位 v-table 指针,并通过它到达 v-table
- 在 v-table 中定位函数指针,并通过它执行调用
与预先知道函数地址(并用符号硬编码)的直接调用相比,这会导致很小的开销。好的编译器设法使它只比常规调用慢 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-table 指针、选择表中的适当条目并最终执行调用所涉及的所有丑陋机制。
但是,如果我们将 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 允许编译器在无法检测到相关函数的情况下避免虚拟调用的开销。