正常的函数调用:
-
复制栈上的一些寄存器,以允许被调用的函数使用这些寄存器;
-
将参数复制到预定义的位置,这样被调用的函数可以找到对应参数;
-
入栈返回地址;
-
跳转到函数的代码,这是一个编译时地址,因为编译器/链接器硬编码为二进制;
-
从预定义的位置获取返回值,并恢复想要使用的寄存器。
而虚函数调用与此完全相同,唯一的区别就是编译时不知道函数的地址,而是:
-
从对象中获取虚表指针,该指针指向一个函数指针数组,每个指针对应一个虚函数;
-
从虚表中获取正确的函数地址,放到寄存器中;
-
跳转到该寄存器中的地址,而不是跳转到一个硬编码的地址。
-
虚函数通常通过虚函数表来实现,在虚表中存储函数指针,实际调用时需要间接访问,这需要多一点时间。
-
然而这并不是虚函数速度慢的主要原因,真正原因是编译器在编译时通常并不知道它将要调用哪个函数,所以它不能被内联优化和其它很多优化,因此就会增加很多无意义的指令(准备寄存器、调用函数、保存状态等),而且如果虚函数有很多实现方法,那分支预测的成功率也会降低很多,分支预测错误也会导致程序性能下降。
例如下面就会有虚函数阻碍编译器内联优化和其他优化
#include <cmath>
#include "timer.h"
struct Base {
public:
virtual int f(double i1, int i2) { return static_cast<int>(i1 * log(i1)) * i2; }
};
int main() {
TimerLog t("timer");
Base *a = new Base();
int ai = 0;
for (int i = 0; i < 1000000000; i++) {
ai += a->f(10, i); // 这里有改动
}
cout << ai << endl;
}
// 436 ms
#include <cmath>
#include "timer.h"
struct Base {
public:
int f(double i1, int i2) { return static_cast<int>(i1 * log(i1)) * i2; }
};
int main() {
TimerLog t("timer");
Base *a = new Base();
int ai = 0;
for (int i = 0; i < 1000000000; i++) {
ai += a->f(10, i); // 这里有改动
}
cout << ai << endl;
}
// 154 ms
例如下面优化本身很难的, 二者速度几乎没有区别
#include <cmath>
#include "timer.h"
struct Base {
public:
virtual int f(double i1, int i2) { return static_cast<int>(i1 * log(i1)) * i2; }
};
int main() {
TimerLog t("timer");
Base *a = new Base();
int ai = 0;
for (int i = 0; i < 1000000000; i++) {
ai += a->f(i, 10); // 这里有改动
}
cout << ai << endl;
}
// 12.895s
#include <cmath>
#include "timer.h"
struct Base {
public:
int f(double i1, int i2) { return static_cast<int>(i1 * log(i1)) * i2; }
};
int main() {
TimerLog t("timer");
Base *a = new Base();
int ai = 0;
for (int i = 0; i < 1000000000; i++) {
ai += a->f(i, 10); // 这里有改动
}
cout << ai << endl;
}
// 12.706s