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

时间:2008-09-23 13:24:33

标签: c++ embedded

嵌入式系统应该避免使用C ++的哪些功能?

请按以下原因对答案进行分类:

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

编辑:让我们使用64k ram的ARM7TDMI作为目标来控制答案的范围。

17 个答案:

答案 0 :(得分:18)

RTTI和异常处理:

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

<强>模板:

  • 如果代码大小有问题,请小心它们。如果您的目标CPU没有或只有非常小的指令缓存,它也可能会降低性能。 (如果不小心使用,模板往往会膨胀代码)。 Otoh聪明的元编程也可以减少代码大小。他没有明确的答案。

虚拟功能和继承:

  • 这些对我来说没问题。我在C中编写了几乎所有嵌入式代码。这并没有阻止我使用函数指针表来模仿虚函数。他们从未成为一个性能问题。

答案 1 :(得分:13)

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

答案 2 :(得分:10)

例外情况可能是避免什么的最常见答案。大多数实现具有相当大的静态内存成本或运行时内存成本。他们也倾向于使实时保证更难。

查看here,了解为嵌入式c ++编写的编码标准的一个很好的例子。

答案 3 :(得分:5)

答案 4 :(得分:4)

这是早期Rationale

Embedded C++ standrard的有趣读物

在EC ++上也可以看到article

嵌入式C ++标准版是C ++的一个合适子集,即没有添加内容。删除了以下语言功能:

  • 多重继承
  • 虚拟基类
  • 运行时类型信息(typeid)
  • 新样式演员表(static_cast,dynamic_cast,reinterpret_cast和 const_cast)
  • 可变类型限定符
  • 命名空间
  • 例外
  • 模板

wiki page上注意到Bjarne Stroustrup所说的(EC ++标准),“据我所知,EC ++已经死了(2004年),如果它不应该是它。” Stroustrup继续推荐Prakash回答引用的document

答案 5 :(得分:3)

使用ARM7并假设您没有外部MMU,动态内存分配问题可能难以调试。我将“明智地使用new / delete / free / malloc”添加到指南列表中。

答案 6 :(得分:3)

如果您使用的是ARM7TDMI,不惜一切代价避免unaligned memory accesses

基本的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上,这可能会打印类似“7ARM”或“ITDM”的内容,假设变量“x”在32位边界上对齐(这取决于“x”所在的位置以及编译器选项正在使用等)你正在使用little-endian模式。它是未定义的行为,但它几乎保证不会按照你想要的方式工作。

答案 7 :(得分:2)

在大多数系统中,您不希望使用 new / delete ,除非您使用自己的托管堆中的实现覆盖它们。是的,它会工作,但你正在处理一个内存受限的系统。

答案 8 :(得分:1)

关于代码膨胀,我认为罪魁祸首更可能是内联而不是模板。

例如:

// foo.h
template <typename T> void foo () { /* some relatively large definition */ }

// b1.cc
#include "foo.h"
void b1 () { foo<int> (); }

// b2.cc
#include "foo.h"
void b2 () { foo<int> (); }

// b3.cc
#include "foo.h"
void b3 () { foo<int> (); }

链接器很可能会将'foo'的所有定义合并到一个翻译单元中。因此,'foo'的大小与任何其他命名空间函数的大小没有区别。

如果您的链接器没有这样做,那么您可以使用显式实例化为您执行此操作:

// foo.h
template <typename T> void foo ();

// foo.cc
#include "foo.h"
template <typename T> void foo () { /* some relatively large definition */ }
template void foo<int> ();        // Definition of 'foo<int>' only in this TU

// b1.cc
#include "foo.h"
void b1 () { foo<int> (); }

// b2.cc
#include "foo.h"
void b2 () { foo<int> (); }

// b3.cc
#include "foo.h"
void b3 () { foo<int> (); }

现在考虑以下事项:

// foo.h
inline void foo () { /* some relatively large definition */ }

// b1.cc
#include "foo.h"
void b1 () { foo (); }

// b2.cc
#include "foo.h"
void b2 () { foo (); }

// b3.cc
#include "foo.h"
void b3 () { foo (); }

如果编译器决定为你内联'foo',那么你将得到3个不同的'foo'副本。看不到任何模板!

编辑:来自InSciTek Jeff上面的评论

对于您只知道将使用的函数使用显式实例化,您还可以确保删除所有未使用的函数(与非模板情况相比,这实际上可能减少代码大小):

// a.h
template <typename T>
class A
{
public:
  void f1(); // will be called 
  void f2(); // will be called 
  void f3(); // is never called
}


// a.cc
#include "a.h"

template <typename T>
void A<T>::f1 () { /* ... */ }

template <typename T>
void A<T>::f2 () { /* ... */ }

template <typename T>
void A<T>::f3 () { /* ... */ }

template void A<int>::f1 ();
template void A<int>::f2 ();

除非你的工具链完全被破坏,否则上面只会为'f1'和'f2'生成代码。

答案 9 :(得分:1)

时间函数通常取决于操作系统(除非你重写它们)。使用您自己的功能(特别是如果您有RTC)

只要你有足够的代码空间,就可以使用

模板 - 不要使用它们

异常也不是非常便携

写入缓冲区的printf函数不可移植(您需要以某种方式连接到文件系统以使用printf写入FILE *)。仅使用sprintf,snprintf和str *函数(strcat,strlen),当然还有广泛的char corespondents(wcslen ...)。

如果速度是问题,也许您应该使用自己的容器而不是STL(例如std :: map容器以确保密钥相等 2(是2)与'less'运算符(a [小于] b == false&amp;&amp; b [小于] a == false表示a == b)。'less'是std :: map类接收的唯一比较参数(而且不仅仅是)。这可能会导致关键例程中的性能损失。

模板,例外是增加代码大小(你可以肯定这一点)。有时甚至性能会因代码更大而受到影响。

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

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

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

因此,对于64k内存,我会说C ++具有一些很好的功能(STL,异常等)可能有点过分。我肯定会选择C.

答案 10 :(得分:1)

我不会说这对此有一个硬性规定;这在很大程度上取决于你的应用。嵌入式系统通常是:

  • 更多限制了他们可用的内存量
  • 通常在较慢的硬件上运行
  • 倾向于更接近硬件,即以某种方式驱动它,如摆弄寄存器设置。

就像其他任何发展一样,你应该平衡你提到的所有要点与你给出/得出的要求。

答案 11 :(得分:1)

使用了GCC ARM编译器和ARM自己的SDT后,我有以下评论:

  • ARM SDT产生更紧,更快 代码但非常昂贵(&gt; Eur5k 每个座位!)。在我以前的工作我们 使用这个编译器就可以了。

  • GCC ARM工具运行良好 虽然这是我自己使用的 项目(GBA / DS)。

  • 使用“拇指”模式,因为这会减少 代码大小显着。在ARM的16位总线变体(例如GBA)上,还有速度优势。

  • 对于C ++来说,64k非常小 发展。我会用C&amp;汇编 在那种环境中。

在这么小的平台上,你必须要小心堆栈的使用。避免递归,大型自动(本地)数据结构等。堆使用也是一个问题(new,malloc等)。 C将使您更好地控制这些问题。

答案 12 :(得分:0)

确保您知道嵌入式平台的编译器支持哪些功能,并确保您了解平台的特性。例如,TI的CodeComposer编译器不执行自动模板实例化。因此,如果要使用STL的排序,则需要手动实例化五个不同的事物。它也不支持流。

另一个例子是你可能正在使用DSP芯片,它没有浮点运算的硬件支持。这意味着每次使用浮点数或双精度数都需要支付函数调用的成本。

总而言之,了解有关嵌入式平台和编译器的所有信息,然后您将了解要避免的功能。

答案 13 :(得分:0)

对于嵌入式系统,您主要希望避免运行成本明显异常的事情。一些示例:例外和RTTI(包括dynamic_casttypeid)。

答案 14 :(得分:0)

使用ATMega GCC感到惊讶的一个特殊问题3.something:当我向其中一个类添加虚拟余烬函数时,我不得不添加一个虚拟析构函数。此时,链接器要求删除操作符(void *)。我不知道为什么会发生这种情况,并为该运算符添加一个空的定义解决了这个问题。

答案 15 :(得分:0)

如果您使用的是针对嵌入式开发或特定嵌入式系统的开发环境,那么它应该已经为您限制了一些选项。根据目标的资源功能,它将关闭一些上述项目(RTTI,例外等)。这是更容易的路线,而不是记住会增加大小或内存需求的内容(尽管如此,你应该在心理上了解这一点)。

答案 16 :(得分:0)

请注意,例外费用取决于您的代码。在我分析的一个应用程序(ARM968上相对较小的应用程序)中,异常支持增加了2%的执行时间,代码大小增加了9.5 KB。在这个应用程序中,仅在出现严重错误的情况下抛出异常 - 即从未在实践中 - 这使得执行时间开销非常低。