嵌入式系统中的C ++使用
嵌入式系统中应避免C ++的哪些功能?
请按以下原因对答案进行分类:
- 内存使用情况
- 代码大小
- 速度
- 可移植性
编辑:让我们使用64k ram的ARM7TDMI作为控制答案范围的目标。
解决方案
异常可能是避免情况的最常见答案。大多数实现都有相当大的静态内存成本或者运行时内存成本。它们也往往使实时保证更加困难。
在这里查找为嵌入式c ++编写的编码标准的一个很好的示例。
RTTI和异常处理:
- 增加代码大小
- 降低效能
- 通常可以用更便宜的机制或者更好的软件设计来代替。
范本:
- 如果代码大小是个问题,请与他们保持谨慎。如果目标CPU没有或者只有很小的nstruction缓存,则它也可能会降低性能。 (如果不加小心使用,模板往往会使代码膨胀)。 Otoh聪明的元编程也可以减少代码大小。他没有明确的答案。
虚函数和继承:
- 这些对我来说很好。我几乎用C编写了所有嵌入式代码。这并没有阻止我使用函数指针表来模拟虚拟函数。他们从来没有成为性能问题。
我不会说这有一个严格的规定。这在很大程度上取决于应用程序。嵌入式系统通常是:
- 他们可用的内存量受到更多限制
- 通常在较慢的硬件上运行
- 倾向于更接近硬件,即以某种方式驱动它,例如摆弄寄存器设置。
就像其他任何开发一样,我们应该在所提到的所有要点与给定/派生的要求之间取得平衡。
如果我们正在使用针对嵌入式开发或者特定嵌入式系统的开发环境,那么它应该已经为我们提供了一些限制。根据目标的资源能力,它会关闭一些上述项(RTTI,异常等)。这是一条更简单的方法,而不是记住会增加大小或者内存要求的内容(尽管无论如何,我们都应该在心中了解这一点)。
对于嵌入式系统,我们将主要要避免发生具有确定的异常运行时成本的事情。一些示例:异常和RTTI(包括dynamic_cast和typeid)。
确保我们知道嵌入式平台的编译器支持哪些功能,并确保我们知道平台的特性。例如,TI的CodeComposer编译器不执行自动模板实例化。结果,如果要使用STL的排序,则需要手动实例化五种不同的东西。它还不支持流。
另一个例子是我们可能正在使用DSP芯片,该芯片不支持浮点运算的硬件。这意味着每次使用浮点数或者双精度数时,我们都要支付函数调用的费用。
总之,了解有关嵌入式平台和编译器的所有知识,然后我们将知道要避免使用哪些功能。
关于代码膨胀,我认为罪魁祸首比模板更可能是内联的。
例如:
// 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',那么我们将得到'foo'的3个不同副本。看不到模板!
编辑:从上面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"生成代码。
选择避免使用某些功能应始终由软件在硬件所限制的条件下,通过对软件在硬件上的行为以及所选择的工具链进行定量分析来推动。在C ++开发中,有许多常规知识"不要",这些知识是基于迷信和古老的历史,而不是硬数据。不幸的是,这通常会导致编写很多额外的变通方法代码,以避免使用某人曾经在某个地方遇到问题的功能。
使用ARM7并假设我们没有外部MMU,动态内存分配问题可能很难调试。我会将"明智地使用new / delete / free / malloc"添加到准则列表中。
文档"信息技术编程
语言,其环境和
系统软件接口技术
《 C ++性能报告》还提供了一些有关嵌入式设备的C ++编程的良好信息。
一个特殊的问题使ATMega GCC 3令我感到惊讶:某些事情:当我向一个类中添加了虚拟余烬函数时,我不得不添加一个虚拟析构函数。此时,链接器要求删除运算符delete(void *)。我不知道为什么会这样,并且为该运算符添加一个空定义解决了这个问题。
如果我们使用的是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"的位置以及正在使用的编译器选项)等等),并且我们使用的是小端模式。这是不确定的行为,但可以保证不会按照我们想要的方式工作。
请注意,异常的代价取决于代码。在我分析的一个应用程序中(ARM968上的应用程序相对较小),对异常的支持使执行时间增加了2%,并且代码大小增加了9.5 KB。在此应用程序中,仅在发生严重不良情况的情况下才引发异常-即从来没有实践过-这使执行时间开销非常低。
时间函数通常取决于操作系统(除非我们重写它们)。使用我们自己的功能(特别是如果我们有RTC)
只要我们有足够的空间来放置代码,就可以使用模板,不要使用它们
异常不是很容易携带
不写入缓冲区的printf函数不可移植(我们需要以某种方式连接到文件系统,才能使用printf写入FILE *)。仅使用sprintf,snprintf和str *函数(strcat,strlen),当然也要使用宽字符charspondents(wcslen ...)。
如果速度是问题所在,也许我们应该使用自己的容器而不是STL(例如std :: map容器,以确保键相等)是否与" less"运算符进行了2(是2)比较([小于] b == false && b [小于] a == false表示a == b)"小于"是std :: map类(并且不仅如此)接收到的唯一比较参数,这可能会导致某些性能损失在关键的例程中。
模板,例外增加了代码大小(我们可以确定)。有时,使用较大的代码甚至会影响性能。
也可能需要重写内存分配函数,因为它们在许多方面都依赖于OS(尤其是在处理线程安全内存分配时)。
malloc使用_end变量(通常在链接描述文件中声明)来分配内存,但这在"未知"环境中不是线程安全的。
有时我们应该使用Thumb而不是Arm模式。它可以提高性能。
因此,对于64k内存,我想说具有某些出色功能(STL,异常等)的C ++可能会过大。我肯定会选择C。
对于早期的嵌入式C ++标准的基本原理,这是一个有趣的阅读。
另请参阅有关EC ++的本文。
嵌入式C ++ std是C ++的适当子集,即它没有添加内容。删除了以下语言功能:
- 多重继承
- 虚拟基类
- 运行时类型信息(typeid)
- 新样式转换(static_cast,dynamic_cast,reinterpret_cast和const_cast)
- 可变类型限定符
- 命名空间
- 例外情况
- 范本
在Wiki页面上,Bjarne Stroustrup说(对于EC ++ std)说:"据我所知,EC ++已死(2004年),如果不是,那应该是。" Stroustrup继续推荐Prakash的答案所引用的文档。
在大多数系统中,除非我们已使用从我们自己的托管堆中提取的自己的实现覆盖了它们,否则不希望使用new / delete。是的,可以,但是我们正在处理内存受限的系统。
同时使用GCC ARM编译器和ARM自己的SDT时,我有以下评论:
- ARM SDT产生更紧密,更快的代码,但价格非常昂贵(每个席位超过5欧元!)。在我以前的工作中,我们使用了此编译器,一切正常。
- 但是,GCC ARM工具运行得很好,这就是我在自己的项目(GBA / DS)中使用的工具。
- 使用"拇指"模式,因为这样可以大大减少代码大小。在ARM的16位总线变体(例如GBA)上,还具有速度优势。
- 对于C ++开发而言,64k非常小。我会在那种环境下使用C&Assembler。
在如此小的平台上,我们必须小心堆栈的使用。避免递归,大型的自动(本地)数据结构等。堆的使用也将是一个问题(新的,malloc等)。 C将使我们对这些问题有更多的控制。