我很惊讶目前为止一些回应和评论...
继承是否会带来一些性能成本(内存方面)
是的。考虑下面的例子:
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;
};
}
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在他的答案中所发布的那样,分配时间将是这个测试的一个巨大部分。
从示例中修剪分配函数和向量创建/销毁可以得出以下数字:
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的内存,连续分配消除了存储动态对象指针的需要。您可以用更多的方式(指针或索引)跟踪对象的实现,但数组也可以显着减少客户端的分配需求(uint32_t vs 64位架构上的指针),此外还有系统为分配所需的所有簿记(在处理如此多的小分配时非常重要)。
具体而言,在此测试中,这些大小消耗了:
32 bit
267MB for dynamic allocations (worst)
19MB for the contiguous allocations
64 bit
381MB for dynamic allocations (worst)
19MB for the contiguous allocations
这意味着所需的内存减少了十倍以上,并且分配/释放所花费的时间明显优于以前!
静态调度实现与混合或动态调度相比,速度可以快几倍。这通常为优化器提供了更多机会来查看程序并相应地进行优化。
实际上,动态类型往往会导出更多符号(方法、析构函数、虚函数表),这可能会显着增加二进制文件的大小。
假设这是您的实际用例,那么您可以显著改善性能和资源使用情况。我已经提出了许多重要的优化...以防有人认为这样改变设计将被视为“微”优化。