首页 > 代码库 > 函数的效能 & 指向 Member Functions 的指针与其效能

函数的效能 & 指向 Member Functions 的指针与其效能

nonmember friend 或 nonstatic member 或 static member 函数都会被转化为相同的形式, 因此三者的效率完全相同.
另外, inline member function 的效率一直是最高的(前提是简单操作), 优化后的效率更是高, 这是因为编译器会将被视为不变的表达式提到循环之外, 因此只计算一次, inline 函数不只能够节省一般函数所调用的负担, 也提供程序额外的优化机会.
virtual function 和 多重继承 的效率要低于一般函数, 要再是虚拟继承那简直是雪上加霜, 原因无非是 virtual function 需要查找 vptr, 而虚拟继承还需要操作 offset.

支持指向 Virtual Member Functions 的指针
考察以下代码:

float (Point::*pmf)() = &Point::Z;Point *ptr = new Point3d;

pmf 是一个指向 member function 的指针, 被设值为Point::Z()(一个 virtual function) 的地址. ptr 则被指定一个 Point3d 对象. 如果我们直接经由 ptr 调用 Z():

ptr->Z();

则被调用的是 Point3d::Z(). 但如果我们从 pmf 间接调用 Z() 呢? 

(ptr->*pmf)();

仍然是 Point3d() 被调用吗? 也就是说, 虚拟机制仍然能够在使用指向 member function 之指针的情况下运行吗? 答案是肯定滴, 问题这是咋实现的呢?
从前几篇博客中可知, 对一个 nonstatic member function 取其地址, 将获得该函数是在内存中的地址. 然而面对一个 virtual function, 其地址在编译时期是未知的, 所能知道的仅是 virtual function 在其相关的 virtual table 中的索引值. 也就是说, 对一个 virtual member function 取其地址, 所能获得的仅是一个索引值.
例如, 假设我们有以下的 Point 声明:

class Point{public:    virtual ~Point();    float X();    float Y();    virtual float Z();    //...};

然后取 destructor 的地址:
&Point::~Point;
得到的结果是 1.取 X() 或 Y() 的地址:
&Point::X();
&Point::Y();
得到的则是函数在内存中的地址, 因为它们不是 virtual. 取 Z() 的地址:
&Point::Z();
得到的结果是 2. 通过 pmf 来调用 Z(), 会被内部转化为一个编译时期的式子:

( *ptr->vptr[ ( int )pmf ] ) (ptr);

对一个指向 member function 的指针评估求值, 会因为该值有两种意义而复杂化; 其调用操作也将有别于常规操作.

//pmf 的内部定义float (Point::*pmf)();

必须允许该函数能够寻址出 novirtual X() 和 virtual Z() 两个 member functions, 而那两个函数有着相同的原型:

//二者都可以被指定给 pmffloat Point::X() {return _x;}float Point::Z() {return 0;}

只不过其中一个代表内存地址, 另一个代表 virtual table 中的索引值. 因此编译器必须定义 pmf 使它能够 1) 含有两种数值
2) 更重要的是其数值可以被区别代表内存的的地址还是 virtual table 中的索引值, 你有好主意吗?
在 cfront 2.0 非正式版中, 这两个值被内含在一个普通的指针内. cfront 如何识别该值是内存地址还是 virtual table 中的 slot 呢? 它使用了如下技巧:

((( int ) pmf ) & ~127)    ?    //nonvirtual invocation    (*pmf)(ptr)    :    //virtual invocation    (*ptr->vptr[ (int) pmf ](ptr ) );

当然, 这种实现技巧必须假设继承体系中最多只有 128 个 virtual functions. 这并不是开始所希望的, 但却证明是可行的. 然而多重继承的引入, 导致需要更一般化的实现方式, 并趁机除去对 virtual functions 的数目限制.

在多重继承之下指向 Member Functions 的指针
为了让指向 member functions 的指针也能够支持多重继承和虚拟继承, Stroustrup 设计了下面一个结构体:

struct __mptr{    int delta;    int index;    union     {        ptrtofun    faddr;        int        v_offset;    };};

这是想表达什么呢? index 和 faddr 分别带有 virtual table 索引和 nonvirtual member function 地址(为了方便, 当index 不指向 virtual table 时, 会被设为 -1). 在该模型之下, 像这样的调用操作:

(ptr->*pmf)();//会变成(pmf.index < 0)    ? //nonvirtual invocation    (*pmf.faddr)(ptr)    : //virtual invocation    (*ptr->vptr[ pmf.index] (ptr));

这种方法所受到的批评是, 每一个调用操作都得付上述成本, 检查其是否为virtual 或 nonvirtual. Microsoft 把这项检查拿掉, 导入一个它所谓的 vcall thunk. 在此策略之下, faddr 被指定的要不就是真正的 memberfunction 地址, 要不就是 vcall thunk 地址. 于是 virtual 或 nonvirtual 函数的调用操作透明化, vcall thunk 会选出并调用相关 virtual table 中适当的 slot.
这个结构体的另一个副作用就是, 当传递一个不变值的指针给 member function 时, 它需要产生一个临时性对象, 就是说, 如果你这么做:

extern Point3d Foo(cosnt Point3d&, Point3d(Point3d::*)());void Bar(const Point3d &p){    Point3d pt = Foo(p, &Point3d::normal);    //其中 &Point3d::normal 的值类似这样: {0, -1, 10727417}    //...}

这将需要产生一个临时对象, 有明确的初值:

//虚拟 C++ 代码__mptr temp = {0, -1, 1027417}Foo(p, temp);

再回到本节一开始的那个结构体, delte 表示 this 指针的 offset 值, 而 v_offset 放置的是一个virtual(或多重继承中的第二或后继的) base class 的 vptr 位置. 如果 vptr 被编一起放在 class 对象的起头处, 这个字段就没有必要了, 代价则是 C 对象的兼容性降低. 这些字段值在多重继承或虚拟继承的情况下才有必要性, 有许多编译器在自身内部根据不同的 classes 特性提供多种指向 member functions 的指针形式. 例如Microsoft 就供应了三种风味:
1, 一个单一继承实例(其中带有 vcall thunk 地址或是函数地址);
2. 一个多重继承实例(其中带有 faddr 和 delta 两个 members);
3. 一个虚拟继承实例(其中带有四个 members).

对于指向 Member Functions 的指针, 多数编译器中都会把以下函数调用:

(pA.*pmf)(pB);

转化为:

pmf.index < 0    ? (*pmf.faddr)(&pA + pmf.delta, pB)    : (*pA.__vptr__Point3d[pmf.index].delta, pB);

其效率在未优化时最高的是 nonmember function 的指针, 多重继承 且 nonvirtual 与 Member nonvirtual function 的效率不相上下, 在优化后前三种指针效率相同. 虚拟继承 且 nonvirtual 优化前的效率略高于 virtual member function, 优化后效率接近, 在不同的编译器上有不同的表现, 即前者的效率不低于后者. 多重继承 且 virtual 毫无疑问是效率最低的, 当你的设计中出现了这种 member function 的指针, 可能你已踏入复杂和无法确定行为的深渊.
以上.

函数的效能 & 指向 Member Functions 的指针与其效能