锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

嵌入式C/C++开发中的代码优化(翻译版)

时间:2024-04-16 10:07:10

事情应该被做得尽可能的简单,但不是任意地简单化。 ――爱因斯坦 虽然做好的程序能按项目需求正确运行到最后一步,但在嵌入式系统开发中并不总是能成功的。由于低成本的需要硬件设计者几乎不可能设计出足够的内存和处理器性能来使得程序能被运行。当然,在软件开发过程中使得程序能够正确运行是更重要的。为了这点,通常有一个或更多的开发平台,这些平台拥有更多的内存和更快的处理器速度,能够使得软件正确运行,并且在项目开发最后阶段能够优化代码。总之,项目开发的最后目标是使得开发出的程序能够在配置低的设备上运行。 1 提高代码的运行效率 所有的C或C++编译器在一定程度上能够优化代码,然而,大部份的优化是基于运行速度和代码长度的折衷,你的程序无法做到既速度快又代码长度小。实际上,在某一方面改进了,但又在其它方面会有负面影响,这取决于程序员决定什么改进是最重要的。设置一些优化方面的信息,编译器在优化阶段当碰到运行速度和代码长度要折衷时能能够做出适当选择。 既然不能让编译器在两方面同时达到优化效果,我建议首先要减小代码量。对于实时或频繁执行的代码块运行速度通常是重要的,并且有许多方法通过动手可以提高运行效率。然而,代码长度通过动手来改变是很难的,这方面编译器能够做得更好。 在程序运行之前,你也许知道哪些子程序或模块是决定代码效率的关键,或许你对这有非常好的解决方法。程序中断、高优先任务、实时计算、高强度计算、频繁被调用函数等都是决定代码效率的关键。有一个存在于软件开发过程中的profiler工具能让你更加专注于那些花费较多时间的程序。 如果你确定需要更好的代码效率,下面的一些技术能让你减少代码运行时间: 内联函数 在C++程序中,“inline”关键字可以在任何一个函数上申明。该关键字能使编译器改变函数调用方式为直接使用拷贝函数内的代码来运行。这种减少运行时间开销的方法与实际函数调用有关系,当内联函数经常被调用且函数内代码少时效果是最好的。 内联函数提供了一个怎样提高运行速度的很好的例子,但链接的代码量有时会有相反的效果。重复增加的代码会增加程序的代码,这与函数的调用次数成正比,显然,对于大函数将增加更加显著。结果是程序运行快了,但需要更多的内存。 表查找 “switch”语句是一个要被小心使用的常用编程技术,每一次的检查和跳转在机器语言中会简单地因决定下一步要做什么而耗尽处理器的时间。为了提高速度,可以根据每一种情况发生的频率来排列每一个“case”的顺序,也就是说,把最容易发生的情况放在前面,把最不容易发生的情况放在后面。虽然在最坏情况下不会改进运行效率,但是可以提高平均运行时间。 如果每一个“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); int (* nodeFunctions[])() = { processNodeA, processNodeB, processNodeC }; . . status = nodeFunctions[getNodeType()](); 用汇编语言写代码 有些软件模块最好汇编语言来写,这给了程序员一个尽可能提高效率的机会。虽然大部份C/C++编译器产生的机器码会比一般的程序员要好,但对同一个函数一个好的程序员仍能够做得比一般的编译器要好。例如,在我的职业生涯早期我用C语言实现一个数字过滤算法并在 TMS320C30 D 目标机上运行,我们使用的编译器不知道也不会利用特殊的指令的优势来做我需要的数学运算,我把C程序中的一个循环代码替换成汇编语言并实现一样的功能,结果程序整个运行时间提高了10多个百分点。 寄存器变量 当申明局部变量时可以用到寄存器,告诉编译器把变量存到一个通用寄存器,而不是在内存栈上。 对于最频繁使用的变量合理使用这个方法来编译能提高一些程序运行效率,函数被调用得越频繁,代码效率就提高得越明显。 全局变量 用全局变量比用传递参数更有效率,用全局变量可以减少因函数调用和退出而产生的参数进栈和出栈。实际上,大部份高效的子程序的实现总是不带参数的。然而,使用全局变量也会对程序产生负面影响,软件工程中一般反对使用全局变量,旨在提高模块化和可重入性,这也是个重要的需要考虑的事项。 轮询 中断服务程序经常被用于提高程序效率。然而, 一些少有的事件会因中断而变得没有效率,这些事件在中断等待时间上处于同一个数据级,这时用轮询的方法与硬件设备通信会更好,当然,这会大大导致小模块化的软件设计。 固定点算法 除非你的目标平台上有浮点运算协处理器,否则你的程序会在处理浮点数据上花费大量开销。支持浮点运算的编译器中的程序库包含一组子程序来模拟浮点运算协处理器中的指令组。这些函数中许多在整数计算上花费很长时间而且这些函数不能被重入。 如果你只在一些计算上用了浮点,用固定点算法来重新实现这些计算会更好。虽然它可能难以实现,但是在理论上用固定点算法是可实现任何浮点计算的。(总之,那是浮点运算软件是怎样实现的事,对吗?)你的最大有利在于你可能不用去实现整个 E 754标准,只是实现一两个计算而已。如果你不需要这类全部的函数,那就坚持使用编译器的浮点运算库并寻找其它提高程序的方法。 2 减少代码量 就如前面我说的,当要减少代码量最好让编译器来为你赌一把。然而,如果最后程序对于你的可用内存来说还是太大,这儿有几个编程技术可以让你减少更多的代码量。在这部份我们将讨论自动和手动来优化代码量。 当然,墨菲法则(任何可能出错的事终将出错)表明第一次让编译器优化先前的代码将会突然失败。也许自动优化的最大坏处是清除没用的代码(dead code elimination),认为这些代码是冗余的和无关的。例如,添加一个值为0的变量,但在无论什么计算中都没有被使用,但你可能仍然要编译器产生那些无关的指令来实现编译器无法知道的某些功能。 例如下面的代码,大部份编译器会去掉第一行语句,因为“*pControl”的值在第三行中被重写前没有被使用: *pControl = DISABLE; *pData = 'a'; *pControl = ENABLE; 但是如果pControl和pData实际上是指向用于内存映射的设备寄存器的的指针,结果会是怎样呢?这种情况下,在写入数据前外围设备无法找到“DISABLE”命令。这将会破坏处理器和处围设备的交互。为了避免出现这个问题,就必须要用“volatile”申明所有指向用于内存映射的设备寄存器的指针和线程(或是一个线程和一个中断程序)共享的全局变量。如果不这样做,墨菲法则在你的项目中将最后会无法预料地出现。我保证会。 注意:不要犯这种错误:以为优化后程序会和原来没有优化的程序一样运行。在每个优化水平上必须完全再次检查你的软件,确认运行结果没有被改变。 情况变量更糟后,调试一个优化后的程序是具有挑战性的。在编译器优化下,一行代码和实现这行代码的处理器指令不是紧密相关的,那些特殊的指令可能已经被除掉或分离开,或者两个相似的代码块可能用同一个方法实现。实际上,一些高级语言中的代码行可能已经被一起除去(如上面的例子中的代码)!结果是,你可能无法在某行设置断点或检查变量的值。 如果你正要自动优化代码,这儿有些能够用手动方法更好地减少代码量的技巧: 避免使用标准库中的子程序 减少代码量的最好方法之一是避免使用标准库中的子程序,这些子程序因为要处理各种可能情况而造成代码量很大。可能可以通过自己实现其中的一部份功能来显著减少代码量。例如,标准C库中“sprintf”函数是非常大的,其中许多是与支持浮点运算有关的代码。但如果你不需格式化和显示浮点数(%f 或 %d),你可以写一个只支持整数处理的“sprintf”函数,这样可以省下几千字节的代码量。实际上,标准C库(如Cygnus'newlib)很少有实现这样的函数,如“sprintf”。 本地字长度 每一个处理器有自己的字大小,ANSI C和C++标准数据类型必须映射成本地字大小。处理更小和更大的数据类型有时需要额外的机器指令。在程序中使用一致的整数可能可以减少几百字节的代码量。 GOTO 语句 和全局变量一样,好的软件工程准则规定反对使用这种方法。但在紧急情况下,GOTO语句能够消除复杂的控制结构或共享一块重复使用的代码。 除了这些技术,前面描述的表查找、写汇编语言代码、寄存器变量、全局变量都对减少代码量有用,其中写汇编语言代码通常能减少最多的代码量。 3 减少内存的使用 在某些情况下,相比较可读写内存(RAM)和只读内存(ROM),RAM才是程序运行的限制因素。在这种情况下,你就得减少对全局数据、栈空间和堆空间的依靠。这些优化程序员比编译器会做得更好。 因为ROM通常比RAM要便宜,减少全局数据的一个可接受的策略是把常量数据移到ROM中,如果你用“const”申明了所有的常量数据,这种方法可以被编译器自动解决。大多C/C++编译器会把遇到的全局常量数据移到一个相当于ROM的指定的数据段中。这个方法对于在运行中有许多不会改变的字符串或数组数据是很有价值的。 如果其中的一些数据在运行中是固定的但不必是常量,则这个不变的数据段可以被放置在混合型内存设备中,这种内存设备可通过网络或一种写入技术来改变其中的数据。比如在你的产品中要配置每个地方的销售税率,如果要改变税率,这种内存设备可以被更新,但另外的RAM也能同时把这更改的数据存起来。 减少栈空间也能降低对RAM的需求。一种方法是准确计算出存储在整个内存空间的栈数据需要多大的栈空间,然后,在一般和不好的状态下运行软件,用 来测试修改后的栈空间,如果栈空间中的数据从没有被重写过,则按计算出的数据来减少栈空间是安全的。 在实时操作系统中要特别注意栈空间,大多操作系统为会每一个任务建立一个独立的栈空间,这些栈被用于函数调用和出现在任务上下文中的子程序服务中断。你可以在用早期的多种形式的描述方法来确定每个任务需要的栈数量。你也可以通过减少任务数量或让操作系统有一个单独的中断栈用于执行所有的子程序服务中断,后一种方法能够显著减少每个任务所需要的栈空间。 全局数据和栈空间占用后剩下的内存空间便是堆空间的限制范围。如果堆空间太小,在需要时则无法分配内存空间,所以在释放前一定要比较“malloc”或“new”的结果是不是等于NULL。如果你采用所有的这些建议,但你的程序仍然需要太大的空间,你可能没有其它选择,只有去减少堆空间了。 4.(略) 原文: 《Programming Embedded Systems in c and C++》O'Reilly,1999 Chapter 10: Optimizing Your Code Things should be made as simple as possible, but not any simpler. —Albert Einstein Though getting the software to work correctly seems like the logical last step for a project, this is not always the case in embedded systems development. The need for low-cost versions of our products drives hardware designers to provide just barely enough memory and processing power to get the job done. Of course, during the software development phase of the project it is more important to get the program to work correctly. And toward that end there are usually one or more "development" boards around, each with additional memory, a faster processor, or both. These boards are used to get the software working correctly, and then the final phase of the project becomes code optimization. The goal of this final step is to make the working program run on the lower-cost "production" version of the hardware. 10.1 Increasing Code Efficiency Some degree of code optimization is provided by all modern C and C++ compilers. However, most of the optimization techniques that are performed by a compiler involve a tradeoff between execution speed and code size. Your program can be made either faster or smaller, but not both. In fact, an improvement in one of these areas can have a negative impact on the other. It is up to the programmer to decide which of these improvements is most important to her. Given that single piece of information, the compiler's optimization phase can make the appropriate choice whenever a speed versus size tradeoff is encountered. Because you can't have the compiler perform both types of optimization for you, I recommend letting it do what it can to reduce the size of your program. Execution speed is usually important only within certain time-critical or frequently executed sections of the code, and there are many things you can do to improve the efficiency of those sections by hand. However, code size is a difficult thing to influence manually, and the compiler is in a much better position to make this change across all of your software modules. By the time your program is working you might already know, or have a pretty good idea, which subroutines and modules are the most critical for overall code efficiency. Interrupt service routines, high-priority tasks, calculations with real-time deadlines, and functions that are either compute-intensive or frequently called are all likely candidates. A tool called a profiler, included with some software development suites, can be used to narrow your focus to those routines in which the program spends most (or too much) of its time. Once you've identified the routines that require greater code efficiency, one or more of the following techniques can be used to reduce their execution time: Inline functions In C++, the keyword inline can be added to any function declaration. This keyword makes a request to the compiler to replace all calls to the indicated function with copies of the code that is inside. This eliminates the runtime overhead associated with the actual function call and is most effective when the inline function is
锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章