在C++的嵌入式环境中使用继承的“代价”是什么?

3

我正在使用C++开始一个新的嵌入式项目,我想知道使用面向接口的设计是否过于昂贵。类似于这样:

typedef int data;

class data_provider {

public:
    virtual data get_data() = 0;
};

class specific_data_provider : public data_provider {
public:
    data get_data() {
    return 7;
    }
};

class my_device {
public:
    data_provider * dp;
    data d;

    my_device (data_provider * adp) {
    dp = adp;
    d = 0;
    }

    void update() {
    d = dp->get_data();
    }
};

int
main() {
    specific_data_provider sdp;
    my_device dev(&sdp);

    dev.update();

    printf("d = %d\n", dev.d);

    return 0;
}

3
与什么相比,它太贵了? - Oliver Charlesworth
1
无论与什么比较,它都永远不会太昂贵;-) 首先是易用性和易维护性。然后是字节、周期和其他因素。 - Andrey Agibalov
与直接编写没有接口的类并直接调用该类的指针而不是接口指针相比,这种方法过于昂贵。 - ivarec
4
@Haole: 当然可以。但是你打算如何模拟多态性?如果你不需要它(这种情况下,比较就不公平),或者你确实需要它(这种情况下,C ++编译器可能会比你手动实现更好)。 - Oliver Charlesworth
啊,抱歉,根据我的经验,开发人员常常会对某些C++特性所带来的内存成本感到惊讶,这就是我回答的原因。 - Tod
4个回答

4

继承本身是免费的。例如,下面的代码中,从性能/内存的角度来看,BC是相同的:

struct A { int x; };
struct B : A { int y; };
struct C { int x, y; };

只有当你使用虚函数时,继承才会产生成本。
struct A { virtual ~A(); };
struct B : A { ... };

在几乎所有实现中,由于虚函数的存在,AB都会比原来大一个指针大小。

与非虚函数相比,虚函数也有其他缺点:

  1. 调用虚函数时需要查找vtable。如果vtable不在缓存中,那么可能会出现L2缓存缺失,这对嵌入式平台来说代价是极高的(例如在当前代游戏机上将超过600个周期)。
  2. 即使命中了L2缓存,如果分支到多个不同的实现,则可能会在大多数调用中导致分支错误预测,从而引起流水线刷新,这又会带来许多周期的开销。
  3. 由于虚函数基本上无法内联(除了少数情况),因此您错过了许多优化机会。如果您要调用的函数很小,则与内联的非虚函数相比,这可能会导致严重的性能惩罚。
  4. 虚函数调用可能会导致代码膨胀。每次虚函数调用都会添加几个字节的指令以查找vtable,并且vtable本身占用了许多字节。

如果使用多重继承,则情况会更糟。

通常人们会告诉您“不要担心性能,直到分析器告诉您需要优化”,但如果性能对您很重要,则这是可怕的建议。如果您不担心性能,则会出现虚函数无处不在的情况,当运行分析器时,没有一个热点需要进行优化-整个代码库都需要进行优化。

我的建议是如果性能很重要,则应进行性能设计。尽可能避免使用虚函数进行设计。围绕缓存设计数据:优先选择数组而不是基于节点的数据结构,例如std::liststd::map。即使您有数千个元素的容器,并且需要频繁插入到中间,我仍然会选择某些架构上的数组。失去的几千个周期用于复制插入数据可能会被实现每次遍历时所获得的缓存本地性所抵消(记住单个L2缓存缺失的成本吗?当遍历链表时,您可以预期会有许多这样的缺失)。


1
他在对另一个回答的评论中说他正在使用Cortex-M3; 没有缓存。但是,它具有单独的Flash和RAM总线,并且在大多数实现中具有简单的Flash加速器,以避免指令获取等待状态。时钟频率不超过120MHz,RAM和Flash通常位于芯片上,因此很少出现内存和处理器之间的速度不匹配,这需要缓存。 - Clifford

4

继承基本上是免费的。然而,多态和动态分派(virtual)有一些后果:带有虚方法的每个类实例都包含一个指向vtable的指针,该指针用于选择正确的调用方法。这为每个虚方法调用添加了两个内存访问。

在大多数情况下,这不会是问题,但在某些实时应用程序中可能成为瓶颈。


+1 直截了当。值得指出的是,在某些情况下,可以使用奇异递归模板模式来克服调用虚拟方法的成本。 - Vitor Py

1

这真的取决于你的硬件。继承本身可能不会花费你任何东西。虚方法会在每个类中的vTable中花费一定量的内存。打开异常处理可能会在内存和性能方面花费更多。我已经在NetBurner平台上使用了C++的所有功能,例如MOD5272等芯片,它们有几兆字节的Flash和8兆字节的RAM。此外,某些事情可能取决于实现,在我使用的GCC工具链上,当使用cout而不是printf时,你会遇到大量的内存消耗(它似乎链接了一堆库)。您可能会对我写的关于类型安全代码成本的博客文章感兴趣。您需要在您的环境中运行类似的测试才能真正回答您的问题。


我使用的是ARM Cortex-M3设备。希望它能处理好。 :) - ivarec

0
通常的建议是先让代码清晰正确,只有在实践中证明存在问题(速度太慢或内存占用过多)时再考虑优化。

这是正确的,但几乎不是 OP 所问的。 - Vinicius Kamakura
那种建议就像是告诉一个桥梁工程师只有在桥梁开始出现裂缝时才担心所选材料一样。如果你想要性能,那么你需要从一开始就为其设计,否则你最终会不得不对你的代码库进行大规模、非平凡的更改。 - Peter Alexander

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