26

在嵌入式系统中应该避免 C++ 的哪些特性?

请按原因对答案进行分类,例如:

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

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

4

16 回答 16

19

RTTI 和异常处理:

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

模板:

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

虚函数和继承:

  • 这些对我来说很好。我几乎所有的嵌入式代码都是用 C 编写的。这并没有阻止我使用函数指针表来模拟虚函数。它们从未成为性能问题。
于 2008-09-23T13:32:54.977 回答
13

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

于 2008-09-23T15:22:24.760 回答
10

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

在此处查找为嵌入式 c++ 编写的编码标准的一个很好的示例。

于 2008-09-23T13:26:39.280 回答
4

这是早期嵌入式 C++ 标准的基本原理的有趣读物

请参阅这篇关于 EC++ 的文章。

Embedded C++ std 是 C++ 的一个真子集,即它没有添加。删除了以下语言功能:

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

Bjarne Stroustrup在wiki 页面上指出(关于 EC++ 标准),“据我所知,EC++ 已死(2004 年),如果不是,它应该是。” Stroustrup 继续推荐Prakash 的回答所引用的文档。

于 2008-09-24T22:57:25.530 回答
3

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

于 2008-09-23T15:42:10.780 回答
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 上,这可能会打印类似“7ARM”或“ITDM”的内容,假设变量“x”在 32 位边界上对齐(这取决于“x”的位置以及正在使用的编译器选项等)并且您使用的是小端模式。这是未定义的行为,但几乎可以保证不会按照您想要的方式工作。
于 2008-09-24T04:40:00.240 回答
2

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

于 2008-09-24T23:41:11.823 回答
1

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

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

就像任何其他开发一样,您应该平衡您提到的所有要点与您给出/派生的要求。

于 2008-09-23T13:33:00.390 回答
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”生成代码。

于 2008-09-23T15:13:31.410 回答
1

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

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

异常也不是很便携

写入缓冲区的printf 函数不可移植(您需要以某种方式连接到文件系统才能使用 printf 写入 FILE*)。只使用 sprintf、snprintf 和 str* 函数(strcat、strlen),当然还有它们的宽字符对应函数(wcslen...)。

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

模板,异常正在增加代码大小(您可以确定这一点)。有时,当代码较大时,甚至性能也会受到影响。

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

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

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

因此,对于 64k 内存,我会说具有一些不错的特性(STL、异常等)的 C++ 可能是矫枉过正。我肯定会选C。

于 2008-09-24T09:05:05.033 回答
1

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

  • The ARM SDT produces tighter, faster code but is very expensive (>Eur5k per seat!). At my previous job we used this compiler and it was ok.

  • The GCC ARM tools works very well though and it's what I use on my own projects (GBA/DS).

  • Use 'thumb' mode as this reduces code size significantly. On 16 bit bus variants of the ARM (such as the GBA) there is also a speed advantage.

  • 64k is seriously small for C++ development. I'd use C & Assembler in that environment.

On such a small platform you'll have to be careful of stack usage. Avoid recursion, large automatic (local) data structures etc. Heap usage will also be an issue (new, malloc etc). C will give you more control of these issues.

于 2008-10-10T08:58:26.397 回答
0

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

于 2008-09-23T13:41:44.860 回答
0

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

于 2008-09-23T13:42:47.167 回答
0

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

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

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

于 2008-09-23T13:55:54.157 回答
0

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

于 2008-09-23T19:18:53.520 回答
0

请注意,异常的成本取决于您的代码。在我分析的一个应用程序(ARM968 上的一个相对较小的应用程序)中,异常支持增加了 2% 的执行时间,并且代码大小增加了 9.5 KB。在这个应用程序中,只有在发生严重错误的情况下才会抛出异常——即在实践中从未发生过——这使得执行时间开销非常低。

于 2008-09-24T08:47:17.570 回答