【问题标题】:C++ usage in embedded systemsC++ 在嵌入式系统中的使用
【发布时间】:2010-09-12 08:48:09
【问题描述】:

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

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

  • 内存使用情况
  • 代码大小
  • 速度
  • 便携性

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

【问题讨论】:

标签: c++ embedded


【解决方案1】:

RTTI 和异常处理:

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

模板:

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

虚函数和继承:

  • 这些对我来说很好。我几乎所有的嵌入式代码都是用 C 编写的。这并不妨碍我使用函数指针表来模拟虚函数。它们从未成为性能问题。

【讨论】:

  • 声明“模板总是内联”是不正确的。这是错误的。如果您没有在函数声明中指定“内联”,它将不会被内联。链接器应该删除重复的实例化。
  • 我仍然会拒绝模板在这里是一个问题。使用模板与在没有模板的情况下编写相同代码的两个版本相比,没有“额外”成本。你有一个明确的例子来说明模板在哪里导致不必要的膨胀?
  • 与模板相关的膨胀与工具链以及它是否可以跨模块识别和整合相同类型的多个实例有很大关系。许多嵌入式工具链不整合。模板中未使用的方法是否得到优化的另一个问题。
  • 关于重复实例化。正如 Avdi 所说,您应该测试您的工具链然后做出反应,但您也可以使用显式实例化。
  • 关于模板方法的优化,再一次,根据严格的 C++,如果你不使用一个方法,它甚至不应该被实例化!您还可以使用显式实例化来强制执行此操作。我将在下面用一个例子更新我的答案。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

    • 答案可能太老了,但该链接不再适合我。有更新的链接吗?我不是 100% 确定,但 this 可能是同一个文档
    【解决方案4】:

    早期的Embedded C++ standrardRationale 读起来很有趣

    在 EC++ 上也可以查看 article

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

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

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

    【讨论】:

    • EC++ 主要是为了解决 C++ 厂商的问题,而不是程序员的问题。
    【解决方案5】:

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

    【讨论】:

    • 另外,动态内存分配发生在运行时,而静态内存由链接器分配。如果可以主要使用静态数据,这将更快(在我一直在研究的一个应用程序中,我从动态切换到静态,执行时间减少了 50%)。
    【解决方案6】:

    如果您使用的是 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”的位置和编译器选项正在使用等)并且您正在使用小端模式。这是未定义的行为,但几乎可以保证不会按照您想要的方式工作。

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

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

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

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

        【讨论】:

          【解决方案9】:

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

          例如:

          // 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”生成代码。

          【讨论】:

            【解决方案10】:

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

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

            异常也不是很便携

            写入缓冲区的

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

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

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

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

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

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

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

            【讨论】:

              【解决方案11】:

              同时使用了 GCC ARM 编译器和 ARM 自己的 SDT,我将拥有以下 cmets:

              • ARM SDT 生成更紧密、更快 代码,但 非常 昂贵(>Eur5k 每个座位!)。在我以前的工作中,我们 用了这个编译器,没问题。

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

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

              • 64k 对于 C++ 来说非常小 发展。我会使用 C 和汇编程序 在那种环境中。

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

              【讨论】:

                【解决方案12】:

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

                【讨论】:

                  【解决方案13】:

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

                  【讨论】:

                    【解决方案14】:

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

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

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

                    【讨论】:

                      【解决方案15】:

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

                      【讨论】:

                        【解决方案16】:

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

                        【讨论】:

                          猜你喜欢
                          • 2016-07-05
                          • 2021-12-04
                          • 1970-01-01
                          • 1970-01-01
                          • 2010-10-08
                          • 1970-01-01
                          • 1970-01-01
                          • 2011-11-28
                          • 1970-01-01
                          相关资源
                          最近更新 更多