白杨

http://baiy.cn

 

“在正确的场合使用恰当的特性” 对称职的C++程序员来说是一个基本标准。想要做到这点,首先要了解语言中每个特性的实现方式及其开销。本文主要讨论相对于传统 C 而言,对效率有影响的几个C++新特性:

相对于传统的 C 语言,C++ 引入的额外开销体现在以下两个方面:

编译时开销

模板、类层次结构、强类型检查等新特性,以及大量使用了这些新特性的 STL 标准库都增加了编译器负担。但是应当看到,这些新机能在不降低,甚至(由于模板的内联能力)提升了程序执行效率的前提下,明显减轻了广大 C++ 程序员的工作量。

用几秒钟的CPU时间换取几人日的辛勤劳动,附带节省了日后调试和维护代码的时间,这点开销当算超值。

当然,在使用这些特性的时候,也有不少优化技巧。比如:编译一个 广泛依赖模板库的大型软件时,几条显式实例化指令就可能使编译速度提高几十倍;恰当地组合使用部分专门化和完全专门化,不但可以最优化程序的执行效率,还可以让同时使用多种不同参数实例化一套模板的程序体积显著减小……

 

运行时开销

运行时开销恐怕是程序员最关心的问题之一了。相对与传统C程序而言,C++中有可能引入额外运行时开销的新特性包括:
  1. 虚基类
  2. 虚函数
  3. RTTI(dynamic_cast和typeid)
  4. 异常
  5. 对象的构造和析构

关于其中第四点:异常,对于大多数现代编译器来说,在正常情况(未抛出异常)下,try块中的代码执行效率和普通代码一样高,而且由于不再需要使用传统上通过返回值或函数调用来判断错误的方式,代码的实际执行效率还可能进一步提高。抛出和捕捉异常的效率也只是在某些情况下才会稍低于函数正常返回的效率,何况对于一个编写良好的程序,抛出和捕捉异常的机会应该不多。关于异常使用的详细讨论,参见:C++编码规范正文中的相关部分和C++异常机制的实现方式和开销分析一节。

而第五点,对象的构造和析构开销也不总是存在。对于不需要初始化/销毁的类型,并没有构造和析构的开销,相反对于那些需要初始化/销毁的类型来说,即使用传统的C方式实现,也至少需要与之相当的开销。这里要注意的一点是尽量不要让构造和析构函数过于臃肿,特别是在一个类层次结构中更要注意。时刻保持你的构造、析构函数中只有最必要的初始化和销毁操作,把那些并不是每个(子)对象都需要执行的操作留给其他方法和派生类去解决。

其实对一个优秀的编译器而言,C++的各种特性本身就是使用C/汇编加以千锤百炼而最优化实现的。可以说,想用C甚至汇编比编译器更高效地实现某个C++特性几乎是不可能的。要是真能做到这一点的话,大侠就应该去写个编译器造福广大程序员才对~

C++之所以 被广泛认为比C“低效”,其根本原因在于:由于程序员对某些特性的实现方式及其产生的开销不够了解,致使他们在错误的场合使用了错误的特性。而这些错误基本都集中在:

  • 把异常当作另一种流控机制,而不是仅将其用于错误处理中
  • 一个类和/或其基类的构造、析构函数过于臃肿,包含了很多非初始化/销毁范畴的代码
  • 滥用或不正确地使用RTTI、虚函数和虚基类机制

其中前两点上文已经讲过,下面讨论第三点。

为了说明RTTI、虚函数和虚基类的实现方式,这里首先给出一个经典的菱形继承实例,及其具体实现(为了便于理解,这里故意忽略了一些无关紧要的优化):


图中虚箭头代表偏移,实箭头代表指针

由上图得到每种特性的运行时开销如下:

 

特性 时间开销 空间开销
RTTI 几次整形比较和一次取址操作(可能还会有1、2次整形加法) 每类型一个type_info对象(包括类型ID和类名称),典型情况下小于32字节

 

虚函数 一次整形加法和一次指针间接引用 每类型一个虚表,典型情况下小于128字节

每对象若干个(大部分情况下是一个)虚表指针,典型情况下小于8字节

 

虚基类 从虚继承的子类中访问虚基类的数据成员或其虚函数时,将增加两次指针间接引用和一次整形加法(部分情况下可以优化为一次指针间接引用)。 每类型一个虚基类表,典型情况下小于32字节

每对象若干虚基类表指针,典型情况下小于8字节

在同时使用了虚函数的时候,虚基类表可以合并到虚表(virtual table)中,每对象的虚基类表指针(vbptr)也可以省略(只需vptr即可)。实际上,很多实现都是这么做的。 这样做的缺点是需要为一些中间类型(如:B1、B2 等)准备多个虚表。

如果指定类型在其类层次结构中只有一个虚基类(大部分使用了虚基类的情况下都是如此,如:上例中就只有 BB 一个虚基类),则可将 vbptr 直接替换为虚基类的偏移地址,这样做将可节省一次指针间接引用,从而提高效率。很多编译器都会自动开启这类优化措施。

此外,由于在很多原本需要访问虚表内 offset 字段的场合中(例如:调用某些虚函数时),该值都是编译时已知的。此时只需一个整形立即数加法即可完成从基类对象到派生类 this 指针的转换。因此,在不怎么影响时间效率的前提下,可以仅保留一个 vbptr 指针(意即:上例中 B2 内的 vbptr 可以被省略)。这种优化方式常常与前文提到的,在单虚基类的场合中将 vbptr 直接替换为虚基类偏址的做法一同使用,以期在时间效率和空间效率间取得较好的平衡,例如:VC 就经常使用这样的优化方式。

 

 * 其中“每类型”或“每对象”是指用到该特性的类型/对象。对于未用到这些功能的类型及其对象,则不会增加上述开销

可见,关于老天“饿时掉馅饼、睡时掉老婆”等美好传说纯属谣言。但凡人工制品必不完美,总有设计上的取舍,有其适应的场合也有其不适用的地方。

C++中的每个特性,都是从程序员平时的生产生活中逐渐精化而来的。在不正确的场合使用它们必然会引起逻辑、行为和性能上的问题。对于上述特性,应该只在必要、合理的前提下才使用。

"dynamic_cast" 用于在类层次结构中漫游,对指针或引用进行自由的向上、向下或交叉强制。"typeid" 则用于获取一个对象或引用的确切类型,与 "dynamic_cast" 不同,将 "typeid" 作用于指针通常是一个错误,要得到一个指针指向之对象的type_info,应当先将其解引用(例如:"typeid(*p);")。

一般地讲,能用虚函数解决的问题就不要用 "dynamic_cast",能够用 "dynamic_cast" 解决的就不要用 "typeid"。比如:

void
rotate(IN const CShape& iS)
{
    if (typeid(iS) == typeid(CCircle))
    {
        // ...
    }
    else if (typeid(iS) == typeid(CTriangle))
    {
        // ...
    }
    else if (typeid(iS) == typeid(CSqucre))
    {
        // ...
    }

// ...
}

以上代码用 "dynamic_cast" 写会稍好一点,当然最好的方式还是在CShape里定义名为 "rotate" 的虚函数。

虚函数是C++众多运行时多态特性中开销最小,也最常用的机制。虚函数的好处和作用这里不再多说,应当注意在对性能有苛刻要求的场合,或者需要频繁调用,对性能影响较大的地方(比如每秒钟要调用成千上万次,而自身内容又很简单的事件处理函数)要慎用虚函数。

需要特别说明的一点是:虚函数的调用开销与通过函数指针的间接函数调用(例如:经典C程序中常见的,通过指向结构中的一个函数指针成员调用;以及调用DLL/SO中的函数等常见情况)是相当的。比起函数调用本身的开销(保存现场->传递参数->传递返回值->恢复现场)来说,一次指针间接引用是微不足道的。这就使得在绝大部分可以使用函数的场合中都能够负担得起虚方法的些微额外开销。

作为一种支持多继承的面向对象语言,虚基类有时是保证类层次结构正确一致的一种必不可少的手段。但在需要频繁使用基类提供的服务,又对性能要求较高的场合,应该尽量避免使用它。在基类中没有数据成员的场合,也可以解除使用虚基类。例如,在上图中,如果类 "BB" 中不存在数据成员,那么 "BB" 就可以作为一个普通基类分别被 "B1" 和 "B2" 继承。这样的优化在达到相同效果的前提下,解除了虚基类引起的开销。不过这种优化也会带来一些问题:从 "DD" 向上强制到 "BB" 时会引起歧义,破坏了类层次结构的逻辑关系。

上述特性的空间开销一般都是可以接受的,当然也存在一些特例,比如:在存储布局需要和传统C结构兼容的场合、在考虑对齐的场合、在需要为一个本来尺寸很小的类同时实例化许多对象的场合等等。

 http://baiy.cn/doc/cpp/inside_rtti.htm

RTTI、虚函数和虚基类的实现方式、开销分析及使用指导(虚函数的开销很小,就2次操作而已)的更多相关文章

  1. C++ primer(十三)--类继承、构造函数成员初始化、虚函数、抽象基类

    一.基类     从一个类派生出另一个类时,原始类称为基类,继承类称为派生类. 派生类对自身基类的private成员没有访问权限,对基类对象的protected成员没有访问权限,对派生类对象的(基类之 ...

  2. 关于MFC中重载函数是否调用基类相对应函数的问题

    在重载CDialog的OnInitDialog()函数的时候,在首行会添加一句:CDialongEx::OnInitDialog();语句,这是为什么呢?什么时候添加,什么时候不添加? 实际上,我们在 ...

  3. 虚函数的使用 以及虚函数与重载的关系, 空虚函数的作用,纯虚函数->抽象类,基类虚析构函数使释放对象更彻底

    为了访问公有派生类的特定成员,可以通过讲基类指针显示转换为派生类指针. 也可以将基类的非静态成员函数定义为虚函数(在函数前加上virtual) #include<iostream> usi ...

  4. 函数形参为基类数组,实参为继承类数组,下存在的问题------c++程序设计原理与实践(进阶篇)

    示例: #include<iostream> using namespace std; class A { public: int a; int b; A(int aa=1, int bb ...

  5. C++ - 虚基类、虚函数与纯虚函数

    虚基类       在说明其作用前先看一段代码 class A{public:    int iValue;}; class B:public A{public:    void bPrintf(){ ...

  6. C++ Pirmer : 第十五章 : 面向对象程序设计之基类和派生的定义、类型转换与继承与虚函数

    基类和派生类的定义以及虚函数 基类Quote的定义: classs Quote { public: Quote() = default; Quote(cosnt std::string& bo ...

  7. C++:抽象基类和纯虚函数的理解

    转载地址:http://blog.csdn.net/acs713/article/details/7352440 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层. ...

  8. C++多态、虚函数、纯虚函数、抽象类、虚基类

    一.C++多态 C++的多态包括静态多态和动态多态.静态多态包括函数重载和泛型编程,动态多态包括虚函数.静态多态是指在编译期间就可以确定,动态多态是指在程序运行时才能确定. 二.虚函数 1.虚函数为类 ...

  9. 4.6 C++抽象基类和纯虚成员函数

    参考:http://www.weixueyuan.net/view/6376.html 总结: 在C++中,可以通过抽象基类来实现公共接口 纯虚成员函数没有函数体,只有函数声明,在纯虚函数声明结尾加上 ...

随机推荐

  1. git SSh key多个key对应多个项目

    必看 1. 本文不教你怎么生成key,主要解决多个项目对应多个SSH KEY的问题,在csdn code库上遇到的人估计很苦恼,为什么多个项目不能用一个key,为什么添加相同的key就会报重复 2. ...

  2. Labview常用快捷键

    对象调整和移动快捷键 Shift-click                          选择多个对象,在现有选择的基础上添加对象 方向键                            ...

  3. php代码20个实用技巧 ------ 转发自菜鸟教程

    1.不要实用相对路径 常常会看到: require_once('../../lib/some_class.php'); 该方法有很多缺点:它首先查找指定的php包含路径,然后查找当前目录,因此会检查过 ...

  4. Mysql中文乱码问题完美解决方案[转]

    原文地址 MySQL会出现中文乱码的原因不外乎下列几点:1.server本身设定问题,例如还停留在latin12.table的语系设定问题(包含character与collation)3.客户端程式( ...

  5. Mysql中时间的操作笔记

    1.创建修改表时,为datetime字段设置当前时间为默认值 CREATE TABLE `NewTable` ( `id` int(11) NOT NULL AUTO_INCREMENT , `des ...

  6. 21个值得收藏的Javascript技巧

    1  Javascript数组转换为CSV格式 首先考虑如下的应用场景,有一个Javscript的字符型(或者数值型)数组,现在需要转换为以逗号分割的CSV格式文件.则我们可以使用如下的小技巧,代码如 ...

  7. fputcsv 和 fgetcsv

    public function putcsv(){ $list = M("ad")->limit(0,10)->select(); $fp = fopen('./fil ...

  8. Xcode Coule not launch "aaa" press launch failed:timed out waiting for app launch

    遇见这个问题 可能是 由于 runapp 的时候设置里面 设置为release了. 解决办法是:见图 build configuration 设置成 debug 状态就OK了. 要是上面的不行就试一下 ...

  9. windbg命令学习1

    一.windbg 常用知识: 1. Windbg中的调试命令,分为三种:基本命令,元命令和扩展命令.基本命令和元命令是调试器自带的,元命令总是以“.”开头,而扩展命令是外部加入的,总是以感叹号“!”开 ...

  10. Java 覆盖测试工具 :EclEmma

    http://www.eclemma.org/installation.html#manual EclEmma 2.2.1 Java Code Coverage for Eclipse Overvie ...