OpenVINOTM,给你看得见的未来!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 嵌入式系统编程中的代码优化

共6条 1/1 1 跳转至

嵌入式系统编程中的代码优化

菜鸟
2003-02-20 02:29:00    评分
嵌入式系统编程中的代码优化 或许有人有这本书吧? 薄薄的一本, 太贵了:-( 翻译了其中最值得看的一章, 请指正. ***************************************** Programming Embedded Systems in C and C++ Michael Barr Publisher: O''Reilly First Edition, January 1999 ISBN: 1-56592-354-5 ***************************************** 第十章 代码优化 一般而言,项目开发的最后一步是使得软件能够 正常工作;然而对嵌入式系统的开发来说,情况并 非总是如此。出于降低产品成本的要求,硬件设计 人员往往只提供完成任务所必需的内存和处理能力。 当然,在项目的软件开发阶段,让程序能够正确地 运行显得更为重要。为此通常会有一块或几块“开 发板”,每块都配有额外的内存或更快速的处理器, 或两者俱全。这些开发板主要用于保证软件能够正 常工作,因此项目开发的最后阶段就将是代码优化。 这一阶段的目标是使得程序可以在低成本的“产品” 版本的硬件平台上正确运行。 10.1 提高代码的效率 所有现代的C和C++编译器都提供某种程度的代码 优化。然而,编译器所采用的绝大多数优化技术都 涉及在程序执行速度和代码大小之间的权衡。你的 程序能够变得更快或更小,但不可能两者兼得。事 实上,对其中一方面的改进往往对另一方面产生不 利的影响。程序员应该决定究竟哪一方面的改进是 最重要的;这样,在面对速度和大小之间的权衡时, 编译器才能做出正确的优化。 由于你无法使得编译器同时提供这两方面的优化, 我建议让它尽其所能地缩减程序的大小。程序执行 的速度通常仅对某些实时的,或频繁执行的代码段 而言是重要的,而且你有多种方式来手工提高这些 代码的效率。但是,手工改变代码的大小是困难的。 若要缩减所有软件模块的大小,编译器处于更有利 的位置。 当程序运行起来的时候,你可能已经意识到哪些 子程序和模块对于代码的总体性能而言是最关键的, 或者已经有了相当不错的想法。中断服务程序、高 优先级的任务、带有实时期限的运算工作,以及那 些执行集中计算或将被频繁调用的函数都是可能的 候选者。某些软件开发套件中所包含的“profiler” 工具有助于找出程序在其上花费了最多(或太多) 时间的代码段。 一旦确定了需要提高效率的代码段,你可以使用 下列技术中的一种或几种来减少它们的运行时间: inline函数 在C++中,可以在任何函数声明之前加上关键字 inline。该关键字请求编译器使用该函数内部代码 的拷贝替代所有对它的调用。这将消除与函数调用 有关的运行时开销。当inline函数会被频繁调用, 但只包含几行代码时,该方法非常有效。 要说明程序执行速度和代码大小之间是如何反向 关联的,inline函数是一个极好的例子。加入 inline函数所导致程序大小的增加与inline函数被 调用的次数成正比。显然,函数越大,代码大小就 增加得越多。这使得程序运行得更快,但同时也需 要更多的ROM。 查表 switch语句是一种需要小心使用的普通编程技术。 组成机器语言指令序列的每一次测试和跳转将消耗 可观的处理器时间,而仅仅是为了决定下一步要做 的工作。为了加快执行的速度,应该按照事件发生 频率的高低来排列各条case语句;也就是说,把最 可能的情况放在第一,最不可能的情况放在最后。 这可以减少平均执行时间,然而在最坏的情况下完 全无效。 如果在每种情况下都有大量的工作需要完成,用 一张函数指针表来替代整条switch语句会更有效率。 例如,下面的代码块就可以用这种方式进行优化: enum NodeType {NodeA, NodeB, NodeC}; switch (getNodeType()) { case NodeA: . . case NodeB: . . case NodeC: . . } 为了加快执行的速度,我们将用下面的代码来替 代这条switch语句。第一部份用于设置:建立一个 函数指针数组。第二部份用一行代码来替代switch 语句,使其执行得更有效率: int processNodeA(void); int processNodeB(void); int processNodeC(void); /* * Establishment of a table of pointers to functions. */ int (* nodeFunctions[])() = {processNodeA, processNodeB, processNodeC}; /* * The entire switch statement is replaced by the next line. */ status = nodeFunctions[getNodeType()](); 手工编写汇编程序 有些软件模块最好用汇编语言编写。这使得程序 员得以尽可能地提高其效率。虽然大多数C/C++编译 器都能生成比普通程序员写得好得多的机器码,优 秀的程序员在编写某些给定的函数时还是能够比编 译器的平均水平做得更好。例如,在职业生涯的早 期,我曾经用C语言实现过一个数字滤波器,目标平 台是TI TMS320C30 DSP。那时我们所用的编译器不 知道或是无法利用一条特殊指令,该指令能够准确 地执行我所需要的数学运算。通过手工使用内嵌的 汇编指令替代一个循环的C程序来完成相同的功能, 我把整个运算时间减少了十分之一以上。 寄存器变量 在声明局部变量时可以使用register关键字。该 关键字请求编译器把变量存放在通用寄存器中,而 不是堆栈中。明智地使用这一技术能使编译器知道 哪些变量会被最频繁地访问,从而在某种程度上提 高函数的性能。函数被调用得越频繁,这一改变使 代码性能提高得就越多。 全局变量 使用全局变量要比给函数传递参数更有效率。这 消除了函数调用之前的参数压栈和函数返回时的参 数退栈。事实上,对于任何子程序来说,最有效率 的实现方法就是完全不使用参数。然而,使用全局 变量也会对程序产生一些不利的影响。软件工程界 通常不鼓励使用全局变量,目的是提高代码的模块 化程度和可重入性,这些也是重要的考量因素。 轮询 中断服务程序通常可用于提高代码的效率,但在 某些少见的情况下,与中断有关的开销反而会导致 效率的降低。这些情况包括:中断之间的平均间隔 时间与中断的反应时间处于同一数量级上。在这种 情况下,通过轮询方式访问硬件设备可能会更好。 当然,这也将导致软件模块化程度的降低。 定点运算 除非你的目标平台配有浮点数协处理器,否则在 程序中处理浮点数将带来很大的开销。编译器所支 持的浮点库含有一系列软件子程序,用于模拟浮点 数协处理器的指令集。与整数运算相比,许多这类 函数都要花费很长的执行时间,并且可能不可重入。 如果你只需要在很少的计算中使用浮点数,较好 的做法可能是用定点的算法重新实现这些计算本身。 虽然如何做到这一点看起来可能有困难,但在理论 上,任何浮点运算都是有可能用定点算法来实现的。 (毕竟,那正是浮点软件库所做的,不是吗?)你最 大的优势在于,或许不必为了仅仅执行一两个计算 而去实现整个IEEE 754标准。如果你确实需要这方 面的全部功能,那就只能依靠编译器的浮点库,并 寻求其它方法来加速你的程序了。 10.2 缩减代码的大小 如前所述,缩减代码的大小这一任务最好是交给 编译器来完成。但是,如果编译后的程序对于可用 的ROM容量来说仍然过大,你还是可以通过几种编程 技术来进一步缩减程序的大小。本节中,我们将讨 论自动和手工优化代码大小的方法。 当然,正如墨非定律指出的那样,当你第一次使 用编译器的优化功能之后,以前工作正常的程序就 会突然出错。关于自动优化,也许最臭名昭著的就 是“废码删除”。优化工作会除去那些编译器认为 是多余的或无关的代码。例如,给变量加零在运行 时肯定不需要进行任何计算。但是当那些“无关” 的指令是用于完成某些编译器所不知道的功能时, 你也许仍然希望编译器确实能生成这些代码。 例如,在处理下面的代码块时,大多数执行优化 的编译器会把第一条语句删掉,因为*pControl的 值在被覆盖(第三行)之前并未用到。 *pControl = DISABLE; *pData = ''a''; *pControl = ENABLE; 可是,当指针pControl和pData实际上指向的是 存储器映射的设备寄存器时,会发生什么情况呢? 这时,在向外设写入一字节数据之前,它将收不到 “DISABLE”命令。在此后处理器和外设之间的所 有交互过程中,这将潜在地导致严重的错误。为了 避免类似的问题,你必须在声明所有指向存储器映 射寄存器的指针和由多个线程(或者一个线程和一 个中断服务程序)共享的全局变量时,加上关键字 volatile。哪怕你仅仅是漏掉了其中之一,墨非 定律就会在项目的最后几天里回来作祟。我保证。 永远不要错误地假设优化后的程序会和未优化时 一样运行。在每一个新的优化级别上,都必须彻底 地对软件进行重新测试,以确保其行为未被改变。 更糟糕的情况是调试一段优化过的程序——至少, 这具有挑战性。当使用了编译器的优化功能时,源 代码行与用于实现该行的处理器指令组之间的关联 性大为弱化。那些指令可能被移动了,可能被分开 了,或者两段相似的代码如今可能会由同一段指令 来实现。事实上,高级语言源程序中的某些行可能 会被完全删除(如上例)。这样,你就可能无法在 程序中的某一行上设置断点,或者无法察看某个感 兴趣的变量。 当自动优化能正常工作之后,你还可以使用下面 的技巧来进一步手工缩减代码的大小: 避免使用静态库函数 为了缩减程序的大小,最有效的途径之一就是避 免使用那些巨大的静态库函数。许多这些大函数执 行起来代价高昂,因为它们试图处理所有可能的情 况。完全有可能用少得多的代码来自行实现其功能 的一个子集。例如,标准C库函数sprintf是出了名 的大,其中很大一部份被其所需的浮点数处理程序 所占用。然而,如果你并不需要格式化和显示浮点 数(%f和%d),那就可以自行编写只处理整数的 sprintf函数,从而节省数千字节的代码空间。事 实上,标准C函数库的少数实现(例如Cygnus的 newlib)就包含这样一个函数:siprintf。 本地字长 每种处理器都有其本地字长。ANSI C和C++标准 规定,数据类型int必须映射到该字长。在处理更 小或更大的数据类型时,有时会用到额外的机器语 言指令。如果尽可能一致地使用int类型,也许你 就能把程序减小宝贵的数百字节。 goto语句 和全局变量一样,良好的软件工程实践反对使用 这一技术。但在紧要关头,goto语句能够简化复杂 的控制结构,或有助于共用一块被重复再三的代码。 除了这些技术之外,上一节中描述的几种方法可 能也会有所帮助,特别是查表、手工编写汇编程序、 使用寄存器变量和全局变量。其中,手工编写汇编 程序通常能使代码的大小得到最大程度的缩减。 10.3 减少内存的占用 在有些情况下,RAM而非ROM会成为应用中的限制 因素。此时你会希望减少程序对全局数据、栈和堆 的占用。所有这些优化都最好由程序员,而不是由 编译器来完成。 由于ROM通常比RAM便宜(基于每字节),一个旨 在减少全局数据量的可接受的策略是把常量数据移到 ROM中。如果你在声明所有的常量数据时加上了关键 字const,编译器就能自动做到这一点。大多数C/C++ 编译器都会把遇到的所有常量全局数据放到一个特殊 的数据段中,定位器能够识别该段为ROM-able。如 果存在大量在程序运行期间不会发生改变的字符串或 数据表,这种技术将是最有效的。 有些数据在程序运行期间保持不变,但不能被设为 常量。这时可以把常量数据段存放在一种作为替代的 复合存储设备中。该设备能够通过网络或者由指定的 技师进行更新。此类数据的例子是每个产品所在地的 营业税率:当某一处的税率发生变化时,相应的存储 设备才需要更新;这样就节省了额外的RAM。 缩减栈的大小同样能够降低程序对RAM的需求。有 一种方法可用于准确地判定程序需要多大的栈:将为 栈所保留的全部内存区域用一种特殊的数据模式填满, 然后当软件在通常条件和极端条件下各运行了一段适 当的时间之后,用调试器察看被修改过的栈区域。仍 然保有特殊数据模式的栈区域从未被覆盖过,因此可 以安全地裁减掉这部份栈空间。(当然,你可能想在 栈里保留一小段额外的空间,以防测试没有进行足够 长的时间,或者测试没能精确地反映程序运行期间可 能发生的全部情况。绝对不要忘记,栈溢出对软件来 说是一种潜在的致命事件,应该不惜一切代价加以避 免。) 使用实时操作系统时尤其应该注意栈空间。大多数 操作系统会为每个任务创建一个单独的栈。这些栈用 于函数调用,以及发生在任务上下文的中断服务程序。 你可以用前面所讲的方法来确定每个任务所需的栈的 大小。你也可以试着减少任务的数量,或者换用一种 带有单独的“中断栈”的操作系统,这种栈可用于所 有中断服务程序的执行。后一种方法能够显著地减少 每个任务对栈空间的需求。 堆的大小受限于除去所有分配给全局数据和栈空间 的RAM之后,剩余的RAM容量。如果堆过小,程序就可 能分不到其所需的内存。所以在使用malloc函数或 new操作的返回值之前,一定要将其与NULL进行比较。 如果在尝试了所有这些建议之后,程序仍然需要太多 的内存,那么可能别无选择,你只好完全不用堆。 10.4 限制C++的影响 当我决定写这本书时,面临的最大的问题之一就是 是否包含对C++的讨论。尽管我对C++很熟悉,但我还 是使用C语言和汇编语言来编写几乎所有的嵌入式软 件。另外在嵌入式软件界,存在许多关于是否值得为 了使用C++而牺牲性能的争论。一般认为,C++程序会 比纯粹的C程序生成更大的可执行体,而且运行速度 要慢得多。然而C++对程序员来说也有很多优点,我 希望在本书中能介绍其中的一些。所以我最后还是决 定包含对C++的讨论,但在所举的例子中只使用那些 对性能影响最小的特性。 我认为许多读者在进行嵌入式系统编程时都会遇到 这样的问题。在结束本书之前,我将简要地阐释我所 用过的每一个C++特性,并指出一些我没有用到的、 代价高昂的特性。 当然,并非所有C++所引入的特性都对性能有影响。 许多老的C++编译器都集成了一种称为“C-front” 的技术,它能够把C++程序转换成C程序,然后把得到 的结果输入到标准C编译器。该技术是可行的——这 一事实说明编程语言在语法上的差异在程序运行期间 不会或仅产生极少的相关开销。只有最新的C++特性, 例如模板,无法用这种方式处理。(应该清楚的还有, 用C++编译器来编译普通的C程序不会产生负面影响。) 例如,定义一个类是完全无害的。公共和私有的成 员数据和成员函数所构成的序列与一个结构加上一系 列函数原型相比,并没有太大的区别。然而,C++编 译器能够通过public和private关键字来判定哪种类 型的函数调用和数据访问是允许的,哪种是不允许的。 这种判定在编译时完成,因此在程序运行期间不必为 此付出代价。单单对类的引入既不会增加代码的大小, 也不会影响程序的效率。 默认参数同样是无害的。如果函数被调用时未在相 应的位置上给出参数,编译器只会插入用于传递默认 参数值的代码。同样,函数名重载也属于编译期间的 工作。每个同名但参数不同的函数都会在编译过程中 被指定一个唯一的名称。编译器每当在程序中遇到该 函数名时就进行修改,然后由链接器将它们正确地匹 配起来。我在任何例子中都没有用到C++的这一特性, 但我应该能够使用它,并且不影响性能。 操作符重载是另一项我应该但没有用过的C++特性。 当编译器遇到这样的操作符时,只是以相应的函数调 用来取代之。所以在下面的代码中,最后两行是等价 的,其对性能上的影响也就很容易理解了。 Complex a, b, c; c = operator+(a, b); // The traditional way: Function Call c = a + b; // The C++ way: Operator Overloading 构造器和析构器也只会带来轻微的性能损失。C++ 确保仅当每次一个该类型的对象被创建或超出作用范 围时,这些特殊的操作才会分别被调用。然而,这一 小小的开销是为了减少错误而付出的合理的代价。构 造器的使用消除了整整一类与未初始化的数据结构有 关的C语言编程错误。该特性被证明同样有助于隐藏 与一些复杂的类——如定时器和任务——相关的笨拙 的初始化序列。 虚函数同样有着合理的性价比。不必过于详细地深 入讨论什么是虚函数,我们只需要指出如果没有虚函 数,多态将是不可能实现的。而没有了多态,C++就 不再是一种真正的面向对象的语言。虚函数唯一较大 的开销是,在被调用之前需要进行一次额外的内存查 找。普通的函数调用不会受此影响。 根据我的经验,代价过于高昂的C++特性包括模板、 异常处理和运行时类型识别。这三者都对代码的大小 有不利的影响,异常处理和运行时类型识别还会增加 程序执行所需的时间。在决定是否使用这些特性之前, 你可能需要通过一些试验来观察它们会对程序的大小 和速度产生怎样的影响。 ryansheng@sina.com 2003/2/18 [align=right][color=#000066][此贴子已经被作者于2003-2-19 18:32:03编辑过][/color][/align]



关键词: 嵌入式     系统     编程     中的     代码     优化     软件     能够         

工程师
2003-02-20 03:24:00    评分
2楼
鼓励!鲜花一朵!

菜鸟
2003-02-20 18:08:00    评分
3楼
好,很好!翻译也很有功底。谢谢!

菜鸟
2004-07-09 17:17:00    评分
4楼
真不错支持!!!!!

菜鸟
2004-07-12 17:14:00    评分
5楼
好!!! 顶!!!!

菜鸟
2004-07-13 22:21:00    评分
6楼
哈哈,这个老帖现在还有人在顶,谢谢大家的支持。 这本书的中译版是早就有的,奇薄且贵。

共6条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]