在嵌入式系统中应该避免 C++ 的哪些特性?
请按原因对答案进行分类,例如:
- 内存使用情况
- 代码大小
- 速度
- 可移植性
编辑:让我们使用带有 64k ram 的 ARM7TDMI 作为目标来控制答案的范围。
RTTI 和异常处理:
模板:
虚函数和继承:
选择避免某些功能应始终通过对软件行为的定量分析来驱动,在您的硬件上,使用您选择的工具链,在您的领域需要的约束下。C++ 开发中有很多基于迷信和古代历史而不是硬数据的传统智慧“不要做”。不幸的是,这通常会导致编写大量额外的解决方法代码,以避免使用某些人在某个地方曾经遇到过问题的功能。
例外可能是要避免什么的最常见答案。大多数实现具有相当大的静态内存成本或运行时内存成本。他们还倾向于使实时保证更加困难。
在此处查找为嵌入式 c++ 编写的编码标准的一个很好的示例。
请参阅这篇关于 EC++ 的文章。
Embedded C++ std 是 C++ 的一个真子集,即它没有添加。删除了以下语言功能:
Bjarne Stroustrup在wiki 页面上指出(关于 EC++ 标准),“据我所知,EC++ 已死(2004 年),如果不是,它应该是。” Stroustrup 继续推荐Prakash 的回答所引用的文档。
使用 ARM7 并假设您没有外部 MMU,动态内存分配问题可能更难调试。我会在指南列表中添加“明智地使用 new / delete / free / malloc”。
如果您使用的是 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);
在大多数系统中,您不想使用new / delete,除非您使用自己的实现覆盖它们,该实现从您自己的托管堆中提取。是的,它会起作用,但你正在处理一个内存受限的系统。
我不会说这有一个硬性规定。这在很大程度上取决于您的应用程序。嵌入式系统通常是:
就像任何其他开发一样,您应该平衡您提到的所有要点与您给出/派生的要求。
关于代码膨胀,我认为罪魁祸首更有可能是内联而不是模板。
例如:
// 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”生成代码。
时间函数通常依赖于操作系统(除非你重写它们)。使用您自己的函数(尤其是如果您有 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。
在使用了 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.
如果您正在使用针对嵌入式开发或特定嵌入式系统的开发环境,它应该已经限制了您的一些选项。根据您的目标的资源能力,它将关闭一些上述项目(RTTI、异常等)。这是更容易走的路,而不是记住什么会增加大小或内存需求(尽管,无论如何你应该在心理上了解这一点)。
对于嵌入式系统,您主要希望避免具有明确异常运行时成本的事情。一些示例:异常和RTTI(包括dynamic_cast和typeid)。
确保您了解嵌入式平台的编译器支持哪些功能,并确保您了解平台的特性。例如,TI 的 CodeComposer 编译器不执行自动模板实例化。因此,如果要使用 STL 的排序,则需要手动实例化五个不同的东西。它也不支持流。
另一个例子是您可能正在使用一个 DSP 芯片,它没有硬件支持浮点运算。这意味着每次使用浮点数或双精度数时,您都需要支付函数调用的成本。
总而言之,了解有关嵌入式平台和编译器的所有信息,然后您就会知道要避免哪些功能。
ATMega GCC 3.something 让我感到惊讶的一个特殊问题是:当我在我的一个类中添加一个虚拟 ember 函数时,我必须添加一个虚拟析构函数。此时,链接器要求操作符 delete(void *)。我不知道为什么会发生这种情况,并且为该运算符添加一个空定义解决了这个问题。
请注意,异常的成本取决于您的代码。在我分析的一个应用程序(ARM968 上的一个相对较小的应用程序)中,异常支持增加了 2% 的执行时间,并且代码大小增加了 9.5 KB。在这个应用程序中,只有在发生严重错误的情况下才会抛出异常——即在实践中从未发生过——这使得执行时间开销非常低。