嵌入式系统中的C++使用

27

C++在嵌入式系统中需要避免哪些特性?

请按照以下原因分类回答:

  • 内存使用
  • 代码大小
  • 速度
  • 可移植性

编辑:以ARM7TDMI和64k RAM作为目标,来限制回答的范围。


1
该文档"信息技术-编程语言、它们的环境和系统软件接口-C++性能技术报告"还提供了一些关于在嵌入式设备中使用C++编程的好信息。 - jk.
1
我在这个帖子中写了一篇关于使用C++进行嵌入式编程的小文章:https://dev59.com/8U_Ta4cB1Zd3GeqPBour#3451301 希望对你有用。 - Max Kielland
16个回答

21

RTTI 和异常处理:

  • 增加了代码大小。
  • 降低了性能。
  • 通常可以用更便宜的机制或更好的软件设计来替代。

模板:

  • 如果代码大小是一个问题,使用模板需要小心。如果目标 CPU 没有或只有非常小的指令缓存,它也可能会降低性能。(如果不小心使用,模板会使代码膨胀)。另一方面,巧妙的元编程也可以减少代码大小。这个问题没有明确的答案。

虚函数和继承:

  • 对我来说没问题。我编写的几乎所有嵌入式代码都是用 C 写的。这并不能阻止我使用函数指针表来模拟虚函数。它们从未成为性能问题。

3
错误的说法是“模板总是内联的”。这是错误的。如果在函数声明中没有指定“inline”,它将不会被内联。链接器应该删除重复的实例化。 - Richard Corden
1
我仍然认为模板在这里不是问题。使用模板与不使用模板编写两个版本的相同代码并没有“额外”的成本。您是否有一个明确的例子,可以展示模板会导致不必要的膨胀? - Richard Corden
1
与模板相关的膨胀问题很大程度上取决于工具链以及它是否能够识别和合并跨模块的多个相同类型的实例。许多嵌入式工具链不进行合并。另一个问题是模板中未使用的方法是否被优化掉。 - Tall Jeff
1
关于重复实例化。正如Avdi所说,您应该测试您的工具链,然后再做出反应,但您也可以使用显式实例化。 - Richard Corden
1
关于模板方法的优化,根据严格的C++规范,如果你不使用一个方法,它甚至不应该被实例化!你也可以使用显式实例化来强制执行这一点。我将在下面更新我的答案,并提供一个示例。 - Richard Corden
显示剩余2条评论

14

避免使用某些功能应该始终基于对您的软件在您的硬件上,在您选择的工具链下,受您的领域约束时行为的定量分析。在C++开发中有很多传统智慧“不适用”是基于迷信和古代历史而不是硬数据。不幸的是,这经常导致编写大量额外的解决方法代码,以避免使用某些功能,因为某个时候,某个地方的某个人曾经遇到过问题。


11

异常很可能会成为回避的最常见答案。大多数实现都具有相当大的静态内存成本或运行时内存成本。它们也往往使实时保证更加困难。

这里可以看到一个编写给嵌入式C++的编码规范的非常好的例子。


1
可能是一个过时的答案,但是这个链接对我来说已经失效了。有没有更新的链接?我不是100%确定,但是这个可能是同一份文档。 - TylerH

5

阅读Rationale关于早期嵌入式C++标准的理由是一件有趣的事情。

这篇文章也涉及EC++。

嵌入式C++标准是C++的一个合适子集,即它没有增加任何内容。以下语言特性被删除:

  • 多重继承
  • 虚基类
  • 运行时类型信息(typeid)
  • 新样式转换(static_cast、dynamic_cast、reinterpret_cast和const_cast)
  • 可变的类型限定符
  • 命名空间
  • 异常
  • 模板

维基页面上指出,Bjarne Stroustrup表示(关于EC++ std):“据我所知,EC++已经死亡(2004年),如果它还没有死亡,那么它应该死亡。” Stroustrup建议参考Prakash答案引用的文档


EC++主要是为了解决C++供应商的问题,而不是程序员的问题。 - MSalters

3

如果使用ARM7且没有外部MMU,动态内存分配问题可能更难调试。我建议在指南列表中加入“谨慎使用new/delete/free/malloc”。


1
此外,动态内存分配发生在运行时,而静态内存是由链接器分配的。如果可能尽量使用静态数据,这将会更快(在我正在处理的一个应用程序中,我从动态切换到静态,执行时间减少了50%)。 - Tomi Junnila

3
如果使用ARM7TDMI,请务必避免非对齐内存访问
基本的ARM7TDMI核心没有对齐检查,并且在进行非对齐读取时会返回旋转数据。一些实现具有用于引发ABORT异常的额外电路,但如果您没有这些实现,则由于非对齐访问而导致的错误很难排查。
例如:
const char x[] = "ARM7TDMI";
unsigned int y = *reinterpret_cast<const unsigned int*>(&x[3]);
printf("%c%c%c%c\n", y, y>>8, y>>16, y>>24);
  • 在 x86/x64 CPU 上运行,会输出 "7TDM"。
  • 在 SPARC CPU 上运行,会由于总线错误而导致核心转储。
  • 在 ARM7TDMI CPU 上运行,假设变量 "x" 对齐于 32 位边界(这取决于 "x" 的位置和使用的编译器选项等),并且使用小端模式,可能会输出类似于 "7ARM" 或 "ITDM" 的内容。这是未定义的行为,但几乎可以保证不会按照您所希望的方式工作。

2

在大多数系统中,除非您使用自己的实现从自己的托管堆中提取内存,否则不建议使用 new / delete。是的,这可能需要一些工作,但您正在处理内存受限的系统。


1

我不会说在这方面有一个硬性规定;这取决于您的应用程序。嵌入式系统通常具有以下特点:

  • 可用内存受到限制
  • 通常在较慢的硬件上运行
  • 倾向于更接近硬件,即以某种方式驱动它,例如调整寄存器设置。

就像任何其他开发一样,您应该权衡您提到的所有观点并根据给定/派生的要求进行平衡。


1

时间函数通常是依赖于操作系统的(除非你重写它们)。使用自己的函数(特别是如果你有一个RTC)

只要你有足够的代码空间,模板就可以使用 - 否则不要使用它们

异常也不是很可移植

printf函数不写入缓冲区是不可移植的(你需要与文件系统连接才能用printf写入FILE*)。仅使用sprintf、snprintf和str*函数(strcat、strlen),当然还有它们的宽字符对应项(wcslen...)。

如果速度是问题,也许你应该使用自己的容器而不是STL(例如std::map容器,以确保一个键等于2个比较,'less'运算符(a [less than] b == false && b [less than] a == false意味着a == b)。'less'是std::map类(不仅仅是它)接收的唯一比较参数。这可能会导致一些关键例程的性能损失。

模板、异常会增加代码大小(你可以确定这一点)。有时候,即使是性能也会受到更大代码的影响。

内存分配函数可能需要重新编写,因为它们在许多方面(特别是在处理线程安全内存分配时)依赖于操作系统。

malloc使用_end变量(通常在链接器脚本中声明)来分配内存,但在“未知”环境中这不是线程安全的。

有时候你应该使用Thumb而不是Arm模式。这可以提高性能。

所以对于64k内存,我会说C++带有一些很好的功能(STL、异常等),可能过于复杂了。我肯定会选择C。


1

我曾经使用过GCC ARM编译器和ARM自己的SDT,以下是我的评论:

  • ARM SDT生成的代码更紧凑、更快,但价格非常昂贵(每个许可证超过5千欧元!)。在我之前的工作中,我们使用了这个编译器,效果还不错。

  • GCC ARM工具也非常好用,我在自己的项目(GBA/DS)中使用它。

  • 使用“thumb”模式可以显著减小代码大小。在ARM的16位总线变体(如GBA)上,也有速度优势。

  • 64k对于C++开发来说太小了。在这种环境下,我会使用C和汇编语言。

在这样一个小平台上,您必须注意堆栈使用情况。避免递归、大型自动(本地)数据结构等。堆使用也将是一个问题(new、malloc等)。C将为您提供更多控制这些问题的能力。


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