继承的成本是多少?

时间:2011-08-26 20:48:02

标签: c++ oop inheritance

这是一个非常基本的问题,但我仍然不确定:

如果我有一个将被实例化数百万次的类 - 是否建议不从其他类派生它?换句话说,继承是否会带来一些成本(在内存或运行时方面构建或销毁对象),我应该在实践中担心?

示例:

class Foo : public FooBase { // should I avoid deriving from FooBase?
 // ...
};

int main() {
  // constructs millions of Foo objects...
}

7 个答案:

答案 0 :(得分:20)

从类继承在运行时不需要任何费用。

如果在基类中有变量,那么类实例当然会占用更多内存,但是如果它们直接在派生类中并且你没有从任何东西继承,那么它们会占用更多内存。

这不考虑virtual方法,这会导致运行时成本较低。

tl;博士:你不应该担心它。

答案 1 :(得分:5)

到目前为止,我对一些回复/评论感到有些惊讶......

  

继承带来了一些成本(就内存而言)

是。给出:

namespace MON {
class FooBase {
public:
    FooBase();
    virtual ~FooBase();
    virtual void f();
private:
    uint8_t a;
};

class Foo : public FooBase {
public:
    Foo();
    virtual ~Foo();
    virtual void f();
private:
    uint8_t b;
};

class MiniFoo {
public:
    MiniFoo();
    ~MiniFoo();
    void f();
private:
    uint8_t a;
    uint8_t b;
};

    class MiniVFoo {
    public:
        MiniVFoo();
        virtual ~MiniVFoo();
        void f();
    private:
        uint8_t a;
        uint8_t b;
    };

} // << MON

extern "C" {
struct CFoo {
    uint8_t a;
    uint8_t b;
};
}

在我的系统上,大小如下:

32 bit: 
    FooBase: 8
    Foo: 8
    MiniFoo: 2
    MiniVFoo: 8
    CFoo: 2

64 bit:
    FooBase: 16
    Foo: 16
    MiniFoo: 2
    MiniVFoo: 16
    CFoo: 2
  

运行时构造或销毁对象

需要时的额外功能开销和虚拟分派(包括适当的析构函数)。这可能会花费很多,并且可能/不能执行一些非常明显的优化,例如内联。

整个主题要复杂得多,但这会让你了解成本。

如果速度或大小真正关键,那么您通常可以使用静态多态(例如模板)来实现性能与编程之间的良好平衡。

关于cpu性能,我创建了一个简单的测试,它在堆栈和堆上创建了数百万这些类型并调用了f,结果是:

FooBase 16.9%
Foo 16.8%
Foo2 16.6%
MiniVFoo 16.6%
MiniFoo 16.2%
CFoo 15.9%

注意:Foo2来自foo

在测试中,分配被添加到矢量中,然后被删除。没有这个阶段,CFoo就完全被优化了。正如Jeff Dege在他的回答中所说,分配时间将是这项测试的重要部分。

从样本中修剪分配函数和向量create / destroy会产生以下数字:

Foo 19.7%
FooBase 18.7%
Foo2 19.4%
MiniVFoo 19.3%
MiniFoo 13.4%
CFoo 8.5%

这意味着虚拟变体占用CFoo的两倍,以执行其构造函数,析构函数和调用,而MiniFoo的速度约为1.5倍。

当我们分配时:如果您可以使用单一类型进行实现,那么您还可以减少在此方案中必须进行的分配数量,因为您可以分配1M对象的数组,而不是创建一个1M地址,然后用独特的新类型填充它。当然,有专门的分配器可以减轻这个重量。由于分配/空闲时间是此测试的权重,因此显着减少分配和释放对象所花费的时间。

Create many MiniFoos as array 0.2%
Create many CFoos as array 0.1%

另外请记住,MiniFoo和CFoo的大小占每个元素内存的1/4 - 1/8,连续分配消除了存储指向动态对象的指针的需要。然后,您可以在实现中以更多方式(指针或索引)跟踪对象,但是数组也可以显着减少客户端上的分配demends(uint32_t vs 64位arch上的指针) - 加上系统所需的所有簿记对于分配(在处理如此多的小分配时这很重要)。

具体而言,此测试中的大小消耗:

32 bit
    267MB for dynamic allocations (worst)
    19MB for the contiguous allocations
64 bit
    381MB for dynamic allocations (worst)
    19MB for the contiguous allocations

这意味着所需内存减少了十多个,分配/释放所花费的时间明显好于此!

静态调度实现与混合或动态调度相比可以快几倍。这通常会为优化者提供更多机会来查看更多程序并相应地对其进行优化。

在实践中,动态类型倾向于导出更多符号(方法,dtors,vtable),这可能会显着增加二进制大小。

假设这是您的实际用例,那么您可以显着提高性能和资源使用率。我提出了一些主要的优化方法......以防万一有人认为以这种方式改变设计会被称为“微观优化”。

答案 2 :(得分:3)

很大程度上,这取决于实施。但是有一些共性。

如果继承树包含任何虚函数,编译器将需要为每个类创建一个vtable - 一个带有指向各种虚函数的指针的跳转表。这些类的每个实例都会带有一个指向其类的vtable的隐藏指针。

任何对虚函数的调用都将涉及隐藏的间接级别 - 而不是跳转到链接时已解析的函数地址,调用将涉及从vtable读取地址然后跳转到该地址。 / p>

一般来说,除了时间最关键的软件之外,这种开销不太可能被测量。

OTOH,你说你要实例化并摧毁数以百万计的这些物体。在大多数情况下,最大的成本不是构造对象,而是为它分配内存。

IOW,您可能会因为使用自己的自定义内存分配器而受益。

http://www.cprogramming.com/tutorial/operator_new.html

答案 3 :(得分:3)

我认为我们所有人都像单独的狼一样编程太多了.. 我们忘记了维护成本+可读性+扩展功能。 这是我的看法

继承成本++

  1. 在较小的项目上:开发时间增加。易于编写所有全球数独代码。总是让我花更多的时间,写一个类继承来做right_thing。
  2. 在较小的项目上:修改时间增加。修改现有代码以确认现有界面并不总是很容易。
  3. 设计时间增加。
  4. 由于多个消息传递,程序效率稍低,而不是暴露的内容(我的意思是数据成员。:))
  5. 仅通过指向基类的指针进行虚函数调用,只有一个额外的解引用。
  6. 就RTTI而言,空间损失很小
  7. 为了完整起见,我将补充一点,太多类会添加太多类型,无论它有多小,都必然会增加编译时间。
  8. 根据基类对象和运行时系统的所有对象跟踪多个对象也有成本,这显然意味着代码大小的轻微增加+由于异常委派机制(无论您是否使用它)而导致的轻微运行时性能损失或不)。
  9. 你不必以PIMPL的方式不自然地扭动你的手臂,如果你想做的就是让你的界面功能的用户绝对不会被重新编译。 (这是一个沉重的代价,请相信我。)
  10. 继承成本 -

    1. 随着程序大小增加超过1/2万行,继承更易于维护。如果您是唯一一个编程,那么您可以轻松地将代码推送到没有对象的代码达到4k / 5k行。
    2. 错误修复的成本降低。
    3. 您可以轻松扩展现有框架,以应对更具挑战性的任务。
    4. 我知道我是一个小恶魔的拥护者,但我认为我们必须公平。

答案 4 :(得分:2)

如果您需要Foo中的FooBase功能,您可以派生或使用合成。派生具有vtable的成本,而FooBase具有指向FooBase,FooBase和FooBase的vtable的指针的成本。所以他们(大致)相似,你不必担心继承的成本。

答案 5 :(得分:2)

创建派生对象涉及为所有基类调用构造函数,并且销毁它们会调用这些类的析构函数。然后,成本取决于这些构造函数的作用,但如果您没有派生但在派生类中包含相同的功能,则需要支付相同的成本。就内存而言,派生类的每个对象都包含其基类的对象,但同样,它与内存使用完全相同,就像您只是在类中包含所有这些字段而不是派生它一样。

要小心,在很多情况下,编写一个更好的主意(拥有'base'类的数据成员而不是从中派生),特别是如果你没有覆盖虚函数和你'之间的关系' '和'基地'不是“是一种”关系。但就CPU和内存使用而言,这些技术都是等效的。

答案 6 :(得分:2)

事实是,如果你对是否应该继承有疑问,答案是你不应该。继承是该语言中第二大耦合关系。

从性能差异来看,在大多数情况下几乎没有差别,除非你开始使用多重继承,如果其中一个基数具有虚函数,那么如果一个基数具有额外的(最小的,可忽略的)成本,则该调度将具有额外的(最小的,可忽略的)成本。 base子对象未与最终覆盖对齐,因为编译器将添加 thunk 来调整this指针。