函数的效能 & 指向 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, 而那两个函数有着相同的原型:

//二者都可以被指定给 pmf
float 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 的指针, 可能你已踏入复杂和无法确定行为的深渊.
以上.

时间: 2024-12-23 06:14:31

函数的效能 & 指向 Member Functions 的指针与其效能的相关文章

C++对象模型——指向Member Function的指针 (Pointer-to-Member Functions)(第四章)

4.4 指向Member Function的指针 (Pointer-to-Member Functions) 取一个nonstatic data member的地址,得到的结果是该member在 class 布局中的byte位置(再加1),它是一个不完整的值,须要被绑定于某个 class object的地址上,才可以被存取. 取一个nonstatic member function的地址,假设该函数是nonvirtual,则得到的结果是它在内存中真正的地址.然而这个值也是不全然的,它也须要被绑定

C++对象模型——Virtual Member Functions (虚拟成员函数)(第四章)

4.2 Virtual Member Functions (虚拟成员函数) 已经看过了 virtual function的一般实现模型:每一个 class 有一个 virtual table,内含该 class 中有作用的 virtual function的地址,然后每个object有一个vptr,指向 virtual table的所在. 为了支持 virtual function机制,必须首先能够对多态对象有某种形式的"执行期类型判断法(runtime type resolution)&quo

老问题了,函数返回指向常字符串的指针,形如 char *func()

摘自<c专家编程>,作为备忘 1. 直接返回简单的字符串 char *func() { return "Simple string demo!\n"} 这是最简单的解决方案,字符串常量存储在只读存储区,如果字符串需要通过计算得到,这种方式就不能使用 2. 使用全局字符数组 这种方式很容易理解,不多解释.但是全局变量尽量少用! 3. 使用静态数组,形如 char *func() { static char buffer[BUF_SIZE]; ... return buffer

C++ Member Functions的各种调用方式

[1]Nonstatic Member Functions(非静态成员函数) C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率.也就是说,如果我们要在以下两个函数之间作选择: float magnitude3d(const Point3d* _this) {...}; float Point3d::magnitude3d() const {...}; 那么选择member function不应该带来什么额

Virtual Member Functions &amp; Static Member Function

如果一个 normalize() 是一个 virtual member function, 那么以下的调用: ptr->normalize(); 将会被内部转化为: (*ptr->vptr[1])(ptr); 其中:vptr 表示由编译器生成的指针, 指向 virtual table, 它被安插在每一个声明有(或继承自) virtual functinos 的 class object 中. 事实上其名称也会被 mangled, 因为在一个复杂的 class 派生体系中, 可能存在多个 vpt

qsort 函数的使用——对普通数组、指针数组、二维数组中的元素进行排序

在ANSI C中,qsort函数的原型是 #include <stdlib.h> void qsort(void *base, size_t nmemb, size_t size, int (*compar) (const void *, const void *)); 解释:qsort函数对含有nmemb个元素的数组进行排序,而base指针指向数组的第一个元素.这个数组的元素个数由size指定. compar函数对qsort的比较操作进行定义,所以可以定制数字的比较,字符串的比较,甚至结构体

JavaScript闭包中闭包函数this的指向

阅读文章前, 请先阅读阮一峰老师的这篇文章http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html 前人写过的, 而且写得很好, 就没必要重复下去了. 只加一些阮老师的文章里没有说的.顺便总结一下. 引用一句话: "闭包就是functions that return function"(出处已经忘记啦) 闭包的类型: 循环闭包, 函数闭包 闭包的特点: 外部访问函数内部的值, 函数内部变量不被回收

c++中函数中变量内存分配以及返回指针、引用类型的思考

众所周知,我们在编程的时候经常会在函数中声明局部变量(包括普通类型的变量.指针.引用等等). 同时,为了满足程序功能的需要,函数的返回值也经常是指针类型或是引用类型,而这返回的指针或是引用也经常指向函数中我们自己声明的局部变量. 这样,程序在某些情况下就可能存在一定的问题.看似很简单的问题,通过仔细的分析,我们就能够更好的理解c++中内存分配和释放的问题. 好,废话不多说,我们进入正题.首先,简单介绍一下程序的内存区域的分配: 程序的内存分配 ①堆区(heap).这一部分主要是由程序开发人员自己

深入了解Windows句柄到底是什么(句柄是逻辑指针,或者是指向结构体的指针,图文并茂,非常清楚)good

总是有新入门的Windows程序员问我Windows的句柄到底是什么,我说你把它看做一种类似指针的标识就行了,但是显然这一答案不能让他们满意,然后我说去问问度娘吧,他们说不行网上的说法太多还难以理解.今天比较闲,我上网查了查,光是百度百科词条“句柄”中就有好几种说法,很多叙述还是错误的,天知道这些误人子弟的人是想干什么. 这里我列举词条中的关于句柄的叙述不当之处,至于如何不当先不管,继续往下看就会明白: 1.windows 之所以要设立句柄,根本上源于内存管理机制的问题—虚拟地址,简而言之数据的