zoukankan      html  css  js  c++  java
  • More Effective C++ 条款24 了解virtual function,multiple inheritance,virtual base classes,runtime type identification的成本

    1. 要实现C++的每一个语言特性,不同的编译器可能采取不同的方法,其中某些特性(如标题所列)的实现可能会对对象的大小和其member functions的执行速度带来冲击.

    2. 虚函数.

        当通过对象指针或引用调用虚函数时,具体调用哪一个虚函数由指针或引用的动态类型决定,大部分编译器使用vtbls(virtual tables,虚函数表)和vptrs(virtual table pointers,虚函数表指针)来实现这一点.

        vtbl通常是一个函数指针的数组或链表,每一个声明或继承虚函数的类都有自己的vtbl,其中的每一个元素就是该类的各个虚函数的指针.假如以下类:

    class C1{
    public:
        C1();
        virtual void f1();
        virtual int f2(char) const;
        virtual void f3(const string&) const;
        void f4() const;
        ...
    }
    View Code

        C1的vtbl看起来像这样:

    (注意,非虚函数f4和C1 constructor并不在表格中!)

        如果C2继承C1,并重新定义部分虚函数,即:

    class C2: public C1 {
    public:
        C2();
        virtual ~C2(); 
        virtual void f1();
        virtual void f5(char *str); 
        ...
    };
    View Code

        那么C2的vtbl看起来像这样:

        从以上示例可以看出虚函数的第一个成本:必须为每一个拥有虚函数的类消耗一块vtbl空间,用于存储虚函数指针.

        由于每个类只有一个vtbl,这就产生了第二个问题:由于C++支持多文件编译,而每一个目标文件都是独立生成的,因而类的vtbl放在哪一个目标文件就需要选择.编译器厂商倾向于两种阵营:一种暴力式做法就是在每一个需要vtbl的目标文件内都产生一个vtbl副本,最后由链接器剥除重复副本;另一种更常见的勘探式做法将vtbl产生于"内含其第一个non-inline,non-pure虚函数定义式"的目标文件中.因此,先前class C1,C2的vtbl应该分别放在内含C1::~C1和C2::~C2定义式的目标文件中.这中勘探式做法要求类必须有一个non-inline的虚函数,如果所有函数都声明为inline,那么大部分编译器会在每一个"使用了class's vtbl"的目标文件中,在大型系统中,由此造成的代码和内存膨胀是十分可观的.因此,要尽量避免将虚函数声明为inline,事实上,编译器通常会忽略虚函数的inline指示.

        由于每个对象都需要对应其所属类型的vtbl,以便在通过指针或引用调用虚函数时实现动态绑定,这就导致了虚函数的第二个成本:每个对象都内含一vptr,用于指向该类的vtbl.

        vptr被加入对象的某个位置,不同编译器对此实现不同,如果vptr加入到对象尾端(以下都假设vptr加入到尾端),那么对象构造看起来像这样:

        4字节的vptr导致的对象大小膨胀所产生的影响可大可小(与对象大小和运行平台等相关),但较大的对象往往意味着较难塞入一个缓存分页(cache page)或虚内存分页(virtual memory page),也就意味着换页(paging)活动可能会增加.

        综合上述,object和vtbl的关系可能像这样

        考虑这样的程序片段:

    void makeCall(C1* pC1){
        pC1->f1();
    }

        由于指针pC1所值对象的动态类型不确定,因而f1的实体是哪一个也就不确定,但编译器仍然要为f1的调用操作产生可执行代码,完成以下动作:

        1). 根据对象对象的vptr找到其vtbl.此动作成本只有一个偏移调整(offset adjustment,以便获得vptr)和一个指针间接动作(以便获得vtbl).

        2). 找出被调用函数(本例为f1)在vtbl内的对应指针.其成本只是一个偏移(offset)以求进入vtbl数组.

        3). 调用步骤2所得指针指向的函数.

        因此以上对于f1的调用操作,编译器产生的代码可能像这样:

    (pC1->vptr[i])(pC1); //i为f1在vtbl中的下标

        这几乎与一个非虚函数的效率相当.因此虚函数的调用成本基本上和"通过一个函数指针来调用函数"相同,虚函数本身并不构成性能上的瓶颈.

        虚函数真正的运行成本在于和inlining发生互动的时候,原则上不应该对虚函数实行inline:inline意味着"在编译期,将调用端的调用操作被被调用函数的函数本体取代",而virtual则意味着"等待,直到运行时期才知道哪个函数被调用".当编译器面对某个调用动作却无法提前得知哪一个函数该被调用时,就没有能力对该函数实现inline了.因此通过指针或引用的虚函数调用是无法被inline的,由于此等调用行为是常态,所以虚函数事实上等于无法被inline.

    3. 多重继承和虚继承

        多重继承下,"找出对象内的vptrs"会变得比较复杂:此时一个对象内会有多个vptrs(每个base class各对应一个);除了之前所讨论的vtbl,针对base classes而形成的特殊vtbl也会被产生出来.结果虚函数对每一个object和每一个class造成的空间负担又增加了一些,运行期调用成本也有所成长.

        多重继承往往导致virtual base classes(虚拟基类)的需求.在non-virtual base的情况下,如果派生类对于基类有多条继承路径,那么派生类会有不止一个基类部分,让基类为virtual可以消除这样的复制现象.然而虚基类也可能导致另一成本:其实现做法常常利用指针,指向"virtual base class"部分,因此对象内可能出现一个(或多个)这样的指针.

        例如以下多重继承"菱形"结构:

    class A{...};
    class B:virtual public A{...};
    class C:virtual public A{...};
    class D:pulic B,public C{...};
    View Code

        A是个虚基类,B和C都采用虚继承,在某些编译器下,D对象的内存布局可能如下:

        这是在没有虚函数参与的情况下,如果A类有虚函数,那么D的布局类似这样

        上图一个奇怪之处在于明明有4个类,却只有三个vptr,原因在于B和D可以共享同一个vptr,大多数编译器会采取此策略.

    4. RTTI(runtime type identification,运行时类型识别)

        RTTI使得可以在运行时获得objects和classes的相关信息,因此其实现必须需要一些内存来存储那些信息:类型信息用type_info类型的对象存放,可以用typeid操作符取得class对应的type_info对象.

        一个类只需要一份RTTI信息,但必须要使得属于这个类的每个对象都能够取得该信息,这和vtbl的要求相同,因此RTTI的的设计理念便是根据class的vtbl来实现.通常在vtbl索引为0的元素存放一指针,用来指向"该vtbl所对应的class"的相应的type_info对象,因此2中的C1的vtbl实际上可能像这样:

        运用这种实现方法,RTTI的运行成本就只需要在每一个class vtbl内增加一个条目,再加上每个class所需的一个type_info对象空间.

        注意,由于多数编译器利用这种方法实现RTTI,因此要某个类要使用RTTI,就必须有vtbl,而要有vtbl,就必须有虚函数,也就是说,没有虚函数的类是无法使用RTTI的,也就无法啊进行dynamic_cast.

    5. 以下表格是虚函数,多重继承,虚拟基类和RTTI的主要成本摘要

    性质

    对象大小增加

    Class 数据量增加

    Inlining几率降低

    虚函数(Virtual Functions)

    多重继承(Multiple Inheritance)

    虚拟基类(Virtual Base Classes)

    常常

    有时候

    运行时类型识别(RTTI)

         从以上可见,C++要支持面向对象,付出了一定的时间和空间成本,但是也因此实现了更强大易用的功能.如果坚持使用C语言,那么以上功能都必须自己打造.相对于编译器产生的代码,自己打造的东西可能比较没效率,也不够鲁棒性.从效率上来说,自己动手打造未必会比编译器做的更好.当然,有时确实要避免编译器在背后所做的这些工作,比如隐藏的vptr以及"指向virtual base classes"的指针,可能会造成"将C++对象存储于数据库"或"在进程边界间搬移C++对象"时的困难度提高,这是可能就需要手动模拟这些性质.

        对于C++面向对象模型的深入了解可参照《深入探索C++对象模型》.

  • 相关阅读:
    每日优鲜三面:在Spring Cloud实战中,如何用服务链路追踪Sleuth?
    一文就能看懂的Nginx操作详解,你还在查漏补缺吗!
    火花思维三面:说说Redis分布式锁是如何实现的!
    【秋招必备】Dubbo面试题(2021最新版)
    【秋招必备】Elasticsearch面试题(2021最新版)
    熬了一通宵!你竟然都没有弄懂陌陌面试官问的Java虚拟机内存?
    react-native-vector-icons 使用记录
    git
    在iOS项目中嵌入RN代码
    UITabBar 图标上下跳动
  • 原文地址:https://www.cnblogs.com/reasno/p/4840529.html
Copyright © 2011-2022 走看看