低效的 C++,真的是这样吗?

来源:互联网 发布:淘宝有电话客服吗 编辑:程序博客网 时间:2024/05/17 08:30
 摘自:http://www.bmrtech.com/article/2008/081125-1.htm

低效的 C++,真的是这样吗?

C++可以为嵌入式编程人员提供一些优于C语言的显著特点。在刚开始,C++可以简单的作为一个更好的C使用:正如一些C编程者用C++编译器运行代码进行质量检验。这也表明,转向使用C++所带来的利弊并不是绝对的:正如本文所述,你可以选择一些对应用有用的C++功能,而不要其他功能。相比于C,C++更加强大的类型检验可以在编译时发现更多的错误,实现精确的内存控制和开发出更紧凑的执行代码。改用面向对象的方法可以提供更好的调试和维护代码的功能

但是除了这些公认的优点之外,在 C语言编程者中似乎存在这样一个普遍的感觉,那就是编写同样的应用代码C++比C低效。诸如此类常识,不见得是正确的:效率的高低取决于你使用了哪些C++功能和如何使用这些功能。

目标系统和结构

在本次讨论中,我们的目标系统是一个典型的小型嵌入式系统, Flash大小从64K到256K不等,其代码大小是我们的一个主要关注对象。这些程序代码将不使用Runtime Type Information(RTTI)和异常处理功能。前者非常有用但是会导致性能降低,后者也是一个非常强大的C++工具,但是会严重影响内存的使用。

我们所要考虑的 C++的结构如Table 1所示:

为了与完成相同功能的 C代码进行比较,C++的结构和代码均被赋予了一个价格标签。标记为FREE的C++代码并不比用C代码产生额外的负担(FREE并不是表示不产生代码,而是生成的代码大小和C语言差不多一样),CHEAP表示比C差一点,而EXPENSIVE则是比C产生巨大的额外负担。所有这些比较都是在IAR的ARM编译器环境中完成的。

封装 – 信息隐藏

在 C++中,封装是通过类完成的。类相当于C中的结构体,但另外还包括方法(函数)和信息隐藏机制:private部分只能在类CircularBuffer的内部进行访问,而public部分可以被使用该类的所有用户访问。

深入系统底层

C++的一个优点在于它可以写出包含详细注释且方便阅读的代码。编译器以一种不同的方式理解代码,并且进行有效的处理,以下两个范例说明了这一点。

第一个范例说明了如何实现不使用内联的较大的函数,例如 Write 。同时说明了指针 this 如何应用在不同地方,且它必须在 C++的命名范围之内不产生二义性。

在接下来的范例( 3)中,创建一个对象 buf ,作为类 CircularBuffer 的一个实体。创建一个指针 p 指向该对象。一个隐藏的指向该对象的 this指针 ,作为参数传递给对成员函数的每一次的调用。虽然从语法上看函数是通过指针进行调用的,但实际上是直接调用的。

>C++中的函数调用与C语言类似。建立一个指向对象的指针,并且将其传递给成员函数,这些也很类似。相比而言,C++的函数调用和指针的成本是FREE的。

名字空间

在名字空间中,所有可视的名字按组分类。外部的代码如果想要访问名字空间内部的数据,就必须标明名字空间的名字。在本例中就是 Decoders::bitrate。名字空间也没有相应的额外开销。

隐式内联

内联是运行一个函数的复本而不是调用函数本身,乍听之下可能会产生很大开销,但是它避免了函数调用的额外负担(这对小的成员函数更加可观),因此大大降低了代码尺寸。在一个类的内部定义的函数是默认隐式内联的,编译器同样也支持用 C语言的内联。而对C++而言,内联是产生优秀代码所必须的,而其成本同样也是FREE的。

操作符重载

要进行操作符重载,就必须先在类中定义一个标准操作符(例如 +,-,| … )的函数。在本范例中, +号运算符不是定义为一个简单的加法操作,而是用来连接两个循环缓冲。当编译器遇到+号运算符时,实际上是将其编译成一个函数调用。本例中的+号能自如的应用于计算虚数和串连运算,由此可见,操作符重载是简化代码的一个有效的方式,并且其开销也是FREE的。

构造函数和析构函数

当构建一个对象时,系统就会隐含调用一个构造函数。如本例所示,该对象可以是一个成员数据。另外,我们要构建一个 UART 对象时 ,构造函数可用于硬件初始化。而在撤销该对象时,系统又将隐含调用析构函数,收回存储空间,这将有利于内存空间的利用和代码的维护。而除了他们实际包含的代码外,构造函数和析构函数本质上的开销也是 FREE的。

引用

引用主要用作函数的参数,和 C中的指针传递有同样的成本,即是FREE的。

继承和虚函数

当多个类需要共用一些数据和行为时,就可以通过继承来共享这些信息。通过使用虚函数继承,可以将类接口和代码执行分离开来,保持透明以及利于以后的修改。该范例就说明了怎样用继承开发可以播放 MP3,WMA和OOP格式的手持式音乐播放器。

类 Track完成播放一个音轨的功能和特性,规定了track和其他部分的接口。其他的类可以继承Track类。由于它是一个抽象类,所以不能创建Track类对象。在类的声明函数中,=0意味着函数必须在派生类中予以定义。

派生类 MP3Track从基类Track中继承,它必须执行基类中所有带=0的函数。派生类WMATrack和MP3Track基本是一样的,只是它解码的是WMA文件。接口和执行的分离使得在DoMusic中不需要知道正在解码的是什么类型的音轨,因为p->Play()将要调用Mp3Track::Play()还是WmaTrack::Play()取决于指针p指向哪个需要执行的Track。

在实现上,每个对象得到一个指向相应类的 vtable的指针vptr,vtable为每个虚函数分配一个函数指针。

 

我们可以用 C语言以不同的方式来编写该播放器。比如 使用以 Tracktype为表达式的Switch语句,通过查表和函数指针进行函数调用。所有的Artist , Title和Play函数可能都需要这样,这会给每个函数分配一个函数指针表,而不是像C++那样每个类一个vtable表。这样就会把track的细节信息分布到所有代码中去。结果就是,如果想增加新的音轨类型支持,比如说AAC格式支持,就会变得十分艰难。调试和维护也将变得更加复杂。

另一个动力来自于成本开销。 C++中要求虚函数的每个类拥有一个 vtable表,每个对象拥有一个vptr指针,因为调用虚函数必须根据vptr指针在vtable表中查找函数地址。相比而言,C中的函数地址则只需要进行查表。而且在 C++ 中每创建一个对象都要比 C语言中使用的表多四个字节。另外,还需要使用构造函数来设置vptr和vtable初始化数据。

但是,这些代码开销仍然是 CHEAP 的,甚至在很多情况下可以认为 FREE。而使用这项功能将会对以后的代码维护和产品升级十分有利。

模板

模板曾被称之为类型参数化,它有点类似宏,提供了一种在应用程序的多处使用同一段代码实现多种变量应用的方式。它减少了代码编写量并且简化了代码维护:模板的变化会立刻反应在应用模板的每一个实体上。而且模板一旦编写完成就可以作为一段可重用代码,用于其他应用程序中。

模板分为函数模板和类模板两种形式。函数模板更加类似宏,但是它比宏在语法和语意上更加安全。和宏一样,模板每一次调用都会潜在地产生额外的代码。

类模板的好处则更多,每一个模板都创建了一个功能完全的类,甚至该类还可以包含其他模板。

模板为妥善实现异常复杂结构的代码重用提供了可能,但是复杂性提高可能会潜在地增加重要代码的开销,即使不会影响到模版创建者,但至少对于模版使用者来说这种情况一定会存在。

但是好的代码并不需要多少额外的开销:下面的例子说明了模板如何在编译时计数,这比在运行时仅仅产生常数 24而不产生任何代码要好的多

>由于不同模板具有的复杂度相差非常大,它可以是只有简单几行代码的宏扩展,也可以是产生许许多多代码的复杂函数集,所以模板开销的变化也非常大。即模板可以是 FREE,CHEAP以至EXPENSICVE的。

标准模板库

C++的一个部分是标准模板库(STL)。顾名思义,它提供了一个预先编写好的模板资源,运用他们可以十分容易的实现堆栈,列表,队列等数据结构和基于这些数据结构的算法。

许多模板使用了容器的概念,正如下图所示:

容器使用自己独有的指针指向特定的元素。迭代器( Iterator)是一个智能指针,它随着容器的不同而有不同的特性:例如,对于向量容器,迭代器可以在向量容器内随机移动,而对于列表容器,它只能前进一步或者后退一步。

在 STL内,有许多基于迭代器的操作算法,可以分为三个主要类别:其一是不修改容器内容的操作,例如 for_each,find ,和 count;其二是修改容器内容的操作,例如transform , copy , replace , fill , generate 和 remove;其三是对容器内容进行分类的操作,例如sort , lower_bound , binary_search和merge。

相对于手工编写程序,使用 STL编程所花费的时间更短,执行效果也比较理想,但是代码的开销是个问题:STL的实现会占用堆空间,引发空间资源紧缺。使用STL库虽然节省了时间,但是对于代码来说,额外开销是巨大的(EXPENSIVE)。

最后总结

相比于 C,C++中产生最大额外开销的元素是STL。模板的开销非常依赖于执行情况,可以从很严重(EXPENSIVE)到基本没有(FREE)。类,名字空间,内联,运算符重载,构造函数/析构函数和引用都是很有效率的,和实现相同功能的C基本一样,虚函数引起了较多的开销,但是相对来说仍然是很少的(CHEAP)。

对于 C++来说,真正的开销往往来自于编程人员。C++的强大和灵活性可能远高于其他编程语言,这有利于完成紧凑而“低开销”的程序,但同时也可能导致误入歧途而付出昂贵代价。

关于 IAR C/C++ 编译器

IAR Systems 是全球第一家做 C 编译器的公司,先进的技术保证了编译器能产生可靠、紧凑的代码,并且能提供广泛的芯片支持。 IAR 的编译器采用了特别适合嵌入式系统的嵌入式 C++ ,提高了代码的可重复利用率。同时,它还提供了非常方便的编程环境。

•  支持很多芯片特定的编程拓展语言,您可以更好地发挥所选芯片的特性。现成的外设寄存器定义文件能帮助您节省大量的时间和精力;

•  多级速度、代码大小优化,确保产生非常紧凑、高效的代码;

•  不管是否采用面向对象的应用开发方法,你都可以从强大的 C++ 中得益,同时也不会对执行速度或代码大小带来风险。存储属性识别和优化的实时库让 C++ 即使对 8 位芯片而言也不失为一种有吸引力的选择;

•  您可以定制 C/C++ 库来满足应用需求,在性能和内存占用中取得平衡。

IAR Embedded Workbench 集成开发环境包含了 C/C++ 编译器、汇编器、链接器、文件管理器、文本编辑器、工程管理器和 C-SPY 调试器。 IAR Embedded Workbench 无缝集成了所有必要组件,确保您的开发流程流畅而不间断,并支持绝大多数 8 位、 16 位、 32 位的 MCU ,不管选用何种微控制器,其用户开发界面都是统一、直观的。

•  ARM, AVR32 , ColdFire

•  HCS12, M16C, M32C , R32C , V850, 78K, S08

•  8051, AVR, PIC, SAM8, R8C , MSP430 等

关于 IAR 状态机建模工具: visualSTATE

visualSTATE 是一套精致、易用的开发工具, 包含图形设计器、测试工具包,代码生成器和文档生成器, 用于设计、测试和实现基于状态图设计的嵌入式应用。该款工具提供了先进的验证和测试模块,可以产生 100% 与 您系统设计一致的紧凑 C/C++ 代码 ,并 可以在任何 8 位、 16 位、 32 位架构上运行。此外,它革命性地与 IAR Embedded Workbench C/C++ 编译调试环境无缝集成 ,能够真正做到基于硬件去调试状态机模型,并以直观的图形方式反馈出各个设计层面的详细信息。

更多信息,请查阅 www.iar.com