Bugs

MMORPG game develop.

[引用]RTTI、虚函数和虚基类的开销分析及使用指导

RTTI、虚函数和虚基类的开销分析及使用指导

白杨

 

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

C++引入的额外开销体现在以下两方面:

编译时开销

模板、类层次结构、强类型检查等新特性,以及大量使用了这些新特性的C++模板、算法库都明显地增加了C++编译器的负担。但是应当看到,这些新机能在不增加程序执行效率的前提下,明显降低了广大C++程序员的工作量。

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

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

 

运行时开销

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

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

而第五点对象的构造和析构开销也不总是存在。对于不需要初始化/销毁的类型,并没有构造和析构的开销,相反对于那些需要初始化/销毁的类型来说,即使用传统的C方式实现,也至少需要与之相当的开销。

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

C++之所以比C“低效”,其根本原因在于:由于对某些特性的实现方式及其开销不够了解,导致程序员在错误的位置使用了错误的特性。而这些错误基本都集中在:

  • 把异常当作另一种流程控制机制,而不是仅将其用于错误处理中
  • 滥用或不正确地使用RTTI、虚函数和虚基类机制

其中第一点上文已经 讲过,下面讨论第二点。

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


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

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

 

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

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

 

虚基类 从直接虚继承的子类(例如上图中的 "B1" 和 "B2",但不包括 "DD" )中访问虚基类的数据成员或其虚函数时,将增加两次指针引用(大部分情况下可以优化为一次)和一次整形加法。 每类型一个虚基类表,典型情况下小于16字节

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

 

 

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

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

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" 写会稍好一点,当然最好的方式还是在其中每个类里定义名为 "rotate" 的虚函数。

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

作为一种支持多继承的面向对象语言,虚基类有时是保证类层次结构正确的一种必不可少的手段。在需要频繁使用基类提供的服务,又对性能要求较高的场合,应该避免使用虚基类。

在基类中没有数据成员的场合,也可以解除使用虚基类。例如,在上图中,如果类 "BB" 中不存在数据成员,那么 "BB" 就可以作为一个普通基类分别被 "B1" 和 "B2" 继承。这样的优化在达到相同效果的前提下,解除了虚基类引起的开销。不过这种优化也会带来一些问题:从 "DD" 向上强制到 "BB" 时会引起歧义;破坏了类层次结构的逻辑关系。

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

posted on 2008-03-27 17:48 Bugs 阅读(1227) 评论(1)  编辑 收藏 引用

评论

# re: [引用]RTTI、虚函数和虚基类的开销分析及使用指导 2008-03-27 19:10 Bugs

除了原文提到的几点,还是来总结几点吧,
可能我个人比较偏好性能,c++的新特性建议不要随意使用。

1.在设计和抽象过程中,把继承的次数降到最小,避免每次构造对象时,
需要构造很多层父类中的构造函数。

2.尽量使用C风格的类型转换
float f = 3.14;
int i = (int)f;
这样与使用static_cast作用一样,性能没有什么区别,
但dynamic_cast就不一样了,效率上大大低于static_cast,
dynamic_cast可以自己使用ObjType来处理一个对象的正确的造型,
这样做,只是麻烦了点,但有了更多的灵活性,和性能。
CXxx *x = 0;
if( obj->GetType() == TYPE_XXX )
{
x = (CXxx*)obj;
}
其实只有在一般不明确类型的时候才会使用dynamic_cast进行造型的,
但在实际编程中,这样的情况很少。
  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理