C++中深度继承树的性能影响

11

深层继承树(在c++中)是否存在效率劣势,即一组大量的类A、B、C等,其中B继承自A,C继承自B,以此类推。我能想到的一个效率影响是,当我们实例化最底层的类,比如C时,B和A的构造函数也会被调用,这将带来性能影响。


1
你的例子不太合理:如果C -> B -> A,那么很可能C需要A和B的所有功能,因此如果它没有从它们派生出来,它的构造函数将不得不执行A和B的构造函数所做的相同工作。 - James McNellis
@James 构造函数“调用”没有额外开销吗?在我的示例中有3个“调用”,但在你的示例中只有一个。 - SegFault
3
请问您是否在尝试采用糟糕的C#/Java风格,使每个类都派生自同一基础类?希望您没有这么做。 - Nicol Bolas
@Nicol 告诉我为什么这很糟糕。 - SegFault
2
@SegFault:问题是你为什么认为这是一个好主意。没有必要这样做。 - Martin York
3个回答

18

让我们列举需要考虑的操作:

构造/析构

每个构造函数/析构函数都会调用其基类的等效函数。然而,正如James McNellis所指出的那样,你显然要做这些工作。你从A派生并不是因为它在那里。所以这项工作总会以某种方式完成。

是的,这将涉及更多的函数调用。但与任何深层次的类层次结构实际上必须进行的工作相比,函数调用开销微不足道。如果您到了函数调用开销实际上很重要的性能点,我强烈建议您在代码中可能不想做任何构造函数调用。

对象大小

一般来说,派生类的开销为零。虚拟成员的开销是一个指针或者虚继承。

成员函数调用、静态

通过这种方式,我指的是调用非虚拟成员函数,或者使用类名(ClassName::FunctionName语法)调用虚拟成员函数。这两种方法都允许编译器在编译时知道要调用哪个函数。

这个性能与层次结构的大小无关,因为它是在编译时确定的。

成员函数调用、动态

这是指完全期望运行时调用虚拟函数。

在大多数合理的C++实现下,这与对象层次结构的大小无关。大多数实现使用每个类的虚函数表(v-table)。每个对象都有一个作为成员的v-table指���。对于任何特定的动态调用,编译器访问v-table指针,挑选出方法并调用它。由于v-table对于每个类都是相同的,因此对于具有深层次结构的类而言,速度不会比具有浅层次结构的类慢。

虚拟继承会稍微影响一下。

指针转换、静态

这指的是static_cast或任何等效操作。这意味着从派生类到基类的隐式转换,显式使用static_cast或C风格转换等。

请注意,这在技术上包括引用转换。

静态类型转换(向上或向下)的性能与继承层次的大小无关。任何指针偏移将在编译时生成。这对于虚拟继承和非虚拟继承都应该成立,但我不是100%确定。

指针转换, 动态

这显然指的是显式使用dynamic_cast。这通常用于从基类向派生类进行强制类型转换。

dynamic_cast的性能可能会因继承层次的大小而改变。但是,合理的实现应只检查当前类和请求类之间的类。因此,它仅与两者之间的类数量成线性关系,而不是与继承层次中的类数量成线性关系。

Typeof

这意味着使用typeof运算符获取与对象相关联的std::type_info对象。

这个操作的性能与继承层次的大小无关。如果类是虚拟类(具有虚函数或虚基类),则它将从虚表中提取出来。如果它不是虚拟的,则它在编译时被定义。

结论

简而言之,大多数操作与继承层次的大小无关。但是,即使在有影响的情况下,这也不是问题。

我更关心的是您是否需要构建这样的层次结构的设计理念。根据我的经验,这样的层次结构来自两条设计线路。

  1. Java/C#理想中的让所有东西都从一个共同的基类派生出来。这在C ++中是一个可怕的想法,不应该使用。每个对象应该只从它需要的基类派生,并且仅从它需要的基类派生。 C ++是建立在“按需付费”原则上的,而从一个共同的基类派生的做法与此相反。一般来说,您可以使用函数重载(例如使用operator<<转换为字符串)来执行与此类共同基类相关的任何操作,或者您本来就不应该进行此类操作。

  2. 滥用继承。当你应该使用包含时使用继承。继承在对象之间创建了一个“是一个”关系。往往,“有一个”关系(一个对象作为成员拥有另一个对象)更加实用和灵活。它们使数据隐藏更容易,并且不允许用户假装一个类是另一个类。

确保你的设计不违反这些原则。


你有没有关于C++库的参考或示例,这些库很好地使用了“具有”而不是“是”的概念? - sandover

2

这会影响程序员的工作表现,但不会像以前那样糟糕。


2
加入大量的多重继承,你的继承树将变成一片树篱。一旦足够厚,它将阻止其他程序员进入你的代码 ;) - Jeremy Friesner
@JeremyFriesner:同意,不过说实话,我发现明智地使用多重继承比理解大型继承树更容易。 - Nicol Bolas

0

正如@Nicol所指出的,它可能正在执行许多操作。 如果这些是您需要完成的操作,无论设计如何,因为它们都是在最少的周期内将程序从call main转换为exit所必需的精确步骤,则您的设计仅仅是编码清晰度的问题(或者可能是缺乏它:)。

根据我的经验,在性能调优例如此示例中,我经常看到的一个巨大的时间浪费源是数据(即类)结构的过度设计。 奇怪的是,数据结构的理由通常是(猜猜看?)- 性能!

在我的经验中,处理数据结构的关键是尽可能保持简单和规范化。如果它完全规范化,那么对它进行任何单一更改都不会使其不一致。你并不总能实现完全的规范化,在这种情况下,你必须处理数据可能暂时不一致的可能性。
这就是为什么人们编写“通知处理程序”,并且这在面向对象编程中得到鼓励。思路是,如果你在一个地方做出了改变,那么可以触发“自动”传播更改的通知到其他地方,试图维护一致性。
通知的问题在于它们可能失控。仅仅将某个布尔属性从true更改为false就可能引起一场通知风暴,以一种没有一个程序员能够理解的方式穿过数据结构,更新数据库,绘制窗口,压缩文件等等。我经常发现这是大多数时钟周期所花费的地方。
我认为,暂时容忍不一致并定期使用某种扫描进程修复它要简单得多,也更有效率。

数据结构与巨大的低效率相伴而行的另一种方式是,如果数据被某个过程有效地解释以产生某些输出,这在图形方面非常常见。 如果数据变化非常缓慢,则将其“编译”而不是“解释”可能是有意义的。 换句话说,将其转换为更简单的指令集或源代码,然后即时编译它,这样就可以更快地执行以产生所需的输出。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接