嵌入式C++:使用STL还是不使用?

83
我一直是一个嵌入式软件工程师,但通常在OSI协议栈的第3层或第2层。我不是真正的硬件人员。我通常做电信产品,通常是手持/手机,这通常意味着像ARM7处理器这样的东西。
现在我发现自己处于更普遍的嵌入式世界中,在一个小型创业公司里,我可能会转向“不太强大”的处理器(这是主观的部分)-我无法预测哪一个。
我已经阅读了很多关于在嵌入式系统中使用C++ STL的辩论,而且没有明确的答案。有一些小担忧与可移植性有关,还有一些关于代码大小或运行时的问题,但我有两个主要的顾虑: 1. 异常处理;我仍然不确定是否使用它(请参见Embedded C++:使用异常还是不使用?) 2. 我强烈反对在嵌入式系统中使用动态内存分配,因为它可能引入问题。我通常有一个静态分配的缓冲池,只提供固定大小的缓冲区(如果没有缓冲区,系统重置)。当然,STL会进行大量的动态分配。
现在我必须决定是否使用STL-对于整个公司,永远(它要进入一些非常核心的软件)。
我应该选择哪种方式?安全但失去了C++的很多特性(在我看来,这不仅仅是语言定义),可能会在以后遇到问题或现在必须添加大量异常处理和其他代码?
我倾向于选择Boost,但是 1) 我不确定它是否适用于我可能想使用的每个嵌入式处理器,2) 在他们的网站上,他们表示不保证/推荐其中某些部分用于嵌入式系统(尤其是FSMs,这似乎很奇怪)。如果我选择了Boost并且之后发现问题...

9
STL是C++语言的一部分。如果您担心内存问题,可以使用自己的内存管理替换operator new和delete。 - GManNickG
6
你有查看过uSTL吗?http://ustl.sourceforge.net/ - Manuel
1
看起来不错,谢谢。当然,我需要更深入地了解可移植性。 - Mawg says reinstate Monica
3
大多数C++容器都需要一个“allocator”对象,用于指定动态内存的获取位置,你可以非常轻松地完全控制内存。(并非所有东西都需要分配器,但大多数都需要。) - Mooing Duck
1
请查看Meyer有关在嵌入式系统中使用C ++的幻灯片:http://htrd.su/wiki/_media/zhurnal/2013/03/28/scott_meyers._effective_c_in_an_embedded_environment/effectcppemb.pdf - Claudio
12个回答

61

我每天都在工作实时嵌入式系统。当然,我的嵌入式系统定义可能与您的不同。但是我们充分利用STL和异常,并且没有遇到任何无法管理的问题。我们还大量使用动态内存(每秒分配大量数据包等),但尚未需要使用任何自定义分配器或内存池。我们甚至在中断处理程序中使用了C++。我们不使用Boost,仅因为某个政府机构不允许。

根据我们的经验,在嵌入式环境中确实可以使用许多现代C++特性,只要您有头脑并进行自己的基准测试。我强烈推荐您使用Scott Meyer的《Effective C++》第3版以及Sutter和Alexandrescu的《C++ Coding Standards》来帮助您使用C++编写一个健全的编程风格。

编辑:在两年后得到赞成之后,让我发表一篇更新。我们在开发中更进一步,最终我们的代码出现了标准库容器在高性能条件下过慢的情况。在这种情况下,我们确实采用了自定义算法、内存池和简化容器。但这正是C ++的美妙之处,您可以使用标准库并获取其提供的所有好处,以满足90%的用例。遇到问题时您不需要丢弃所有,而是只需对出现问题的地方进行手动优化。


3
+1,有用的回答。但我不认为您对异常或代码膨胀有足够的了解,建议查看我的回复以了解更多信息。 - j_random_hacker
1
在我的回复中确切地哪里出现了“代码膨胀”这个词组?我感谢你的+1,但请把你的评论针对这个特定的答案。 - Brian Neal
非常好听(是的,这两本书(以及完整的Meyers “effective...”)现在就放在我的显示器旁边。你们针对什么类型的处理器? - Mawg says reinstate Monica
1
你现在可以使用Boost吗? - S.S. Anne

37

是否牺牲了很多C++的本质(在我看来,这不仅仅只是语言定义),而且可能会在以后遇到问题或不得不添加大量异常处理和其他代码?

在游戏领域也存在着类似的争论,人们持不同意见。关于引用部分,为什么你会担心失去“很多C++的本质”呢?如果它不实用,那就不要使用它。它是否“C ++”并不重要。

运行一些测试。你能否绕过STL的内存管理以满足你的需求?如果可以,这个努力值得吗?许多STL和boost旨在解决的问题根本不会出现,如果你设计避免随意的动态内存分配... STL解决了你面临的特定问题吗?

许多人在紧凑环境中处理STL并感到满意。很多人只是避免使用它。有些人提出了全新的标准。我认为没有一个正确答案。


1
谢谢,丹,这个和其他的(也被投票赞同)让我真正思考了。由于我们有一个嵌入式系统,我们有自己的内存池。STL对我们最有用的是容器类;但我们在初始化时将它们最大化。因此,要么我们接受这一点并在系统启动后不进行STL分配,要么我们可以使用普通的旧数组(指向静态分配对象的指针)。 - Mawg says reinstate Monica

28
其他帖子已经涉及到了动态内存分配、异常和可能的代码膨胀等重要问题。我只想补充一点:不要忘记 <algorithm>!无论您使用 STL 向量还是普通的 C 数组和指针,您仍然可以使用 sort()binary_search()random_shuffle()、用于构建和管理堆的函数等等。这些例程几乎肯定比您自己构建的版本更快,更少出错。
例如:除非您仔细考虑,否则您自己构建的洗牌算法很可能会产生偏斜的分布random_shuffle() 不会这样。

20

2007年,来自Electronic Arts的Paul Pedriana撰写了一篇详细的文章,阐述了为什么STL在嵌入式控制台开发中不合适以及他们为什么要编写自己的STL。这是一篇详细的文章,但最重要的原因有:

  1. STL分配器速度慢、臃肿且效率低下。
  2. 编译器实际上并不很擅长内联所有那些深层函数调用。
  3. STL分配器不支持显式对齐。
  4. GCC和MSVC的STL中自带的STL算法性能不太好,因为它们非常平台无关,因此缺少许多微优化,这可能产生很大的差异。

几年前,我们公司决定完全不使用STL,而是实现了自己的容器系统,这些容器是最大程度的高效,更易于调试,同时又更节约内存。虽然这是很多工作量,但回报已经多次超过投入。但我们所处的领域,产品竞争的是如何将更多的功能塞入特定CPU和内存大小的16.6毫秒中。

至于异常:在控制台上很慢,任何告诉你相反的人都没有尝试过测量。由于必须生成prolog / epilog代码,启用它们只是简单编译就会减慢整个程序的速度——如果你不相信我,请自行测量。对于顺序CPU来说,情况甚至比x86还要糟糕。因此,我们使用的编译器甚至不支持C++异常。

性能的提升并不仅仅是避免了抛出异常的代价——而是完全禁用了异常。


12
你提供的文章链接来自2006年,现在已经过时。在现代优秀的编译器上,C++异常不会变慢。如果你正在处理一个不存在优秀现代编译器的嵌入式系统,那么你将会遇到问题,但是一概而论地说“异常很慢”是错误的。 - JoeG
15
像Herb Sutter和Andrei Alexandrescu这样的C++专家不同意你关于“异常缓慢”的说法。如果你不使用异常,那么你自己现在就需要编写并检查错误返回代码,而这些代码几乎总是比现代编译器为异常所生成的代码效率低。此外,人们编写的(如果他们真的写了)用于检查错误代码的代码通常充斥着错误和失误。 - Brian Neal
4
异常处理并不会很慢,但是它们确实会在至少一个流行的现代编译器(MSVC++9)中引入非零的运行时开销,即使没有抛出异常。要验证这一点,请尝试使用/EHa/EHsc编译(而不是链接)http://pastebin.com/m1fb29a45,并使用/Fa生成汇编列表。在两种情况下,Win32结构化异常处理(SEH)管理被引入——这意味着额外的数据被推入堆栈并设置了`FS`段寄存器。 - j_random_hacker
10
这篇文章是从2006年的,但是“我的”时间记录是在2009年8月。我已经阅读了所有关于异常不再缓慢的理论,但是它们与我所进行的实际测量并不一致。 - Crashworks
2
被公认为专家的数据分析师不同意“红书盲目阅读总是正确”的说法。 - rama-jka toti
显示剩余2条评论

16

首先声明,我已经好几年没有从事嵌入式工作了,并且也没有使用过C++,因此我的建议只值你所付的钱……

STL使用的模板永远不会生成不必要的代码,所以不必担心代码膨胀。

STL本身不会抛出异常,所以这不应该是一个问题。如果你的类不会抛出异常,那么就应该是安全的。将对象初始化分为两部分,让构造函数创建一个最基本的对象,然后在一个返回错误代码的成员函数中进行任何可能失败的初始化。

我认为所有容器类都允许你定义自己的分配函数,所以如果你想从一个池中进行分配,那么可以实现它。


1
+1,我认为这是少数几次将构造函数之外的构造工作移出构造函数的好主意之一。 - j_random_hacker
6
“STL本身不会抛出异常”是什么意思?如果你使用一个超出范围的索引调用vector::at,会发生什么?而且,你也可以配置IO流来抛出异常。此外,模板可以生成比手写更多的代码。请看Stroustrup关于将模板与void*结合以减少这种膨胀的示例。 - Brian Neal
4
“vector::at()”是一个很好的例子。更准确的说,STL可以被使用到一种方式,以避免产生异常(例如使用operator[]()而不是at()),而且无需做出任何额外的妥协。 - j_random_hacker
@Brian:关于代码膨胀问题,如果你在编译器中指定 /Gy 选项,并在链接器中指定 /OPT:ICF 选项,那么 MSVC++ 将在链接时删除包含相同目标代码的函数。我相信 GNU 链接器也可以做到类似的事情。 - j_random_hacker
1
@Brian Neal,我忘记了vector::at,也许还有其他一些 - 感谢您的澄清。在标准库文件中搜索“throw”应该是可能的,并找到对我过于概括性陈述的所有“异常”。 - Mark Ransom
显示剩余5条评论

11
开源项目"嵌入式模板库(ETL)"针对嵌入式应用中使用STL时遇到的常见问题,提供/实现了一个库:
  • 确定性行为
  • "创建一组容器,在编译时确定其大小或最大大小。这些容器应该与STL中提供的容器基本相同,并具有兼容的API。"
  • 无动态内存分配
  • 无需RTTI
  • 尽可能少地使用虚拟函数(仅在绝对必要时)
  • 一组固定容量的容器
  • 以缓存友好的方式将容器存储为连续分配的内存块
  • 减少容器代码大小
  • 类型安全的智能枚举
  • CRC计算
  • 校验和和哈希函数
  • 变体=一种类型安全的联合
  • 可以选择断言、异常、错误处理程序或不检查错误
  • 经过大量单元测试
  • 源代码有很好的文档
  • 还有其他功能...

您还可以考虑由E.S.R. Labs提供的商业C++嵌入式开发人员STL


5
  1. 对于内存管理,您可以实现自己的分配器,从池中请求内存。而所有STL容器都具有分配器模板。

  2. 对于异常,STL不会引发太多异常,通常最常见的是内存不足。在您的情况下,系统应该重置,因此您可以在分配器中进行重置。其他异常例如超出范围,用户可以避免。

  3. 所以,我认为您可以在嵌入式系统中使用STL:)


4
除了所有的评论之外,我建议您阅读《C++性能技术报告》,其中具体涉及您感兴趣的主题:在嵌入式系统(包括硬实时系统)中使用C ++;异常处理通常如何实现以及它的开销;自由存储分配的开销。
该报告非常好,因为它揭示了许多关于C ++性能的流行谣言。

3
基本上取决于您的编译器和内存量。如果RAM超过几KB,则动态内存分配可以大大帮助。如果您使用的标准库中malloc的实现未针对您的内存大小进行调整,则可以编写自己的实现,或者有很好的示例,例如mm_malloc from Ralph Hempel,您可以使用它来编写新的和删除运算符。
我不同意那些重复异常和stl容器太慢或太臃肿等传言的人。当然,它比简单的C malloc添加了一些代码,但是明智地使用异常可以使代码更加清晰,并避免在C中进行太多的错误检查。
必须记住,STL分配器将以2的幂增加其分配,这意味着有时它会进行一些重新分配,直到达到正确的大小,如果您知道要分配的大小,则可以使用reserve来防止这种情况,因此它变得与所需大小的一个malloc一样便宜。
如果你在一个向量中有一个大的缓冲区,例如,在某个时刻它可能会重新分配并在重新分配和移动数据时使用了1.5倍于你打算使用的内存大小。(例如,在某个时刻,它分配了N个字节,你通过追加或插入迭代器添加数据,它会分配2N个字节,复制前N个字节并释放N个字节。在某个时刻,你分配了3N个字节)。
所以最终它有很多优点,并且只有当你知道自己在做什么时才能付出。你应该知道一些C++的工作原理,以便在嵌入式项目中使用它而不会出现意外情况。
对于那些使用固定缓冲区和重置的人,如果内存不足,你可以在new运算符或其他地方进行重置,但这意味着你设计得不好,可能会耗尽你的内存。
在ARM RealView 3.1中抛出异常:
--- OSD\#1504 throw fapi_error("OSDHANDLER_BitBlitFill",res);
   S:218E72F0 E1A00000  MOV      r0,r0
   S:218E72F4 E58D0004  STR      r0,[sp,#4]
   S:218E72F8 E1A02000  MOV      r2,r0
   S:218E72FC E24F109C  ADR      r1,{pc}-0x94 ; 0x218e7268
   S:218E7300 E28D0010  ADD      r0,sp,#0x10
   S:218E7304 FA0621E3  BLX      _ZNSsC1EPKcRKSaIcE       <0x21a6fa98>
   S:218E7308 E1A0B000  MOV      r11,r0
   S:218E730C E1A0200A  MOV      r2,r10
   S:218E7310 E1A01000  MOV      r1,r0
   S:218E7314 E28D0014  ADD      r0,sp,#0x14
   S:218E7318 EB05C35F  BL       fapi_error::fapi_error   <0x21a5809c>
   S:218E731C E3A00008  MOV      r0,#8
   S:218E7320 FA056C58  BLX      __cxa_allocate_exception <0x21a42488>
   S:218E7324 E58D0008  STR      r0,[sp,#8]
   S:218E7328 E28D1014  ADD      r1,sp,#0x14
   S:218E732C EB05C340  BL       _ZN10fapi_errorC1ERKS_   <0x21a58034>
   S:218E7330 E58D0008  STR      r0,[sp,#8]
   S:218E7334 E28D0014  ADD      r0,sp,#0x14
   S:218E7338 EB05C36E  BL       _ZN10fapi_errorD1Ev      <0x21a580f8>
   S:218E733C E51F2F98  LDR      r2,0x218e63ac            <OSD\#1126>
   S:218E7340 E51F1F98  LDR      r1,0x218e63b0            <OSD\#1126>
   S:218E7344 E59D0008  LDR      r0,[sp,#8]
   S:218E7348 FB056D05  BLX      __cxa_throw              <0x21a42766>

看起来并不可怕,如果异常没有被抛出,在 {} 块或函数内部不会增加额外的开销。


2

来看一下在嵌入式环境中,C++相对于C的一些优势。你并不总是需要异常处理、RTTI或动态内存管理来使用C++。你可以通过编译器选项关闭它们。并非所有STL容器都使用动态内存,除了iostream之外几乎没有使用RTTI。大多数函数都标记为noexcept;这意味着它们不会抛出异常。 C++相对于C的主要优势在于前者将所有工作都推向了编译时,而后者则试图在运行时实现多态性。编译时工作负载意味着能够提早发现错误,从而避免生产灾难。C缺乏用于编译时计算的抽象机制;也没有足够的设施来确保类型安全和安全性。C没有完全取代C++在所有代码库中的唯一原因是心理惯性(包括Linus Torvalds的对C++的恐惧)。如果C++社区尝试反过来为C库提供C++绑定,那么好处现在应该已经很明显了。C++社区应该提供重要库的第一手重写,并提供C绑定以实现向后兼容,以展示C++真正强大的潜力。


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