本来想找译文的,没找到,所以就慢慢记录下来。有不正确的地方,大家交流提出来,我做修改。或者谁那有译文告诉一下,杜绝重复造轮子。
嵌入式软件通常运行在能力有限的处理器上,因此优化代码就很必要。 在本文中,我们主要探讨实时和嵌入式系统的C和C ++代码的优化技术。
1 过早的优化是万恶之源
Donald Knuth曾说:“程序员浪费大量时间来思考或担心程序非关键代码部分的速度,实际上,在调试和维护代码时,这种在效率上的尝试会产生很严重的负面影响(非关键路径优化潜力小 ,你费劲巴拉几天就那么点提高。你能不受负面影响,严重的还打击生活与工作自信心)。我们应该忽略这些小的效率提升点,这大概能占到97%。过早的优化是万恶之源,我们更应该做的是不应该在关键的3%上放弃机会”。 通常,对于大部分代码来说,正确性和可读性方面的考虑胜过代码性能问题。对于一小部分代码,您可能不得不牺牲可读性来提高性能。这种优化应该在项目接近完成时进行。当你有一个能工作的系统时,就会对性能关键代码有了更深入,更全面的理解。
2 调整结构体大小为2的幂
当涉及到结构体数组检索或遍历时,编译器通过执行乘以结构体大小的运算来实现。如果结构尺寸是2的幂次方,乘法运算将被廉价的移位运算所取代。因此,保持结构体大小为2的幂次对齐将大幅提高数组索引的性能。
3 小范围内使用case标签
case标签在小范围内使用,则编译器不会为switch语句生成if-else-if级联逻辑。相反,它会生成case标签的跳转表,并根据switch的值来索引该表。这样生成的代码比在case标签相距较远的情况下生成的if-else-if级联代码更快。并且,基于跳转表的switch语句的性能与switch语句中案例条目的数量无关。
4 常用case标签放在前面,不常用的放置在后面
也就是说switch里的语句,先处理常用的case选项,不常用的放到后面。关于switch-case标签较远,会生成if-else的逻辑,我查阅不到具体阈值,有知道的,留言告知一下。
如果case标签的距离太远,则编译器将生成if-else-if逻辑代码,并与每个case标签进行比较,然后跳转到标签匹配项执行对应动作。通过首先放置最常使用的case标签,可以减少对频繁发生的case执行的比较次数。一般情况,这意味着应该将匹配成功相对应的case放在失败处理的案例之前。应为大多数情况还是先处理正常逻辑流程选项。
5 将大型switch语句分解为嵌套的switch
对于某些编译器,上面的技术不适用,它们不会按照switch语句中指定的顺序生成if-else-if的逻辑(具体谁家的编译器,我也不知道,只是感觉如果有这种编译器,估计早就淘汰,或者改进了吧)。 在这种情况下,可以使用嵌套的switch语句获得较好的效果。为了减少执行比较的次数,请将大型switch语句拆分为嵌套的switch。 将经常发生的case标签放入第一个switch,并将其余不常用case标签放入另一个switch,把后者作为前者的default标签分支。原文是有例子代码的,咱这就免了吧,相信看这种文章的,应该都知道什么意思。
6 局部变量尽量少
如果函数中局部变量的数量较少,则编译器将能够将它们放入寄存器中。 这将避免栈操作。 基于以下两个原因,这可能会导致相当大的改进:
6.1 局部变量都在寄存器中,与从内存访问它们相比,这速度提高的可不是一点半点啊。
6.2 如果不需要在栈上保存任何局部变量,则编译器将不会产生设置和恢复帧指针的开销。
关于第二点,函数跳转,进入函数,首先执行的一定是:
push bp
mov sp, bp
退出函数,要执行:
mov bp,sp
pop bp
我做的实验中,结论如下:
A 函数中没有局部变量,正常编译,都有上面的设置和恢复帧指针的操作;
B 编译时候如果用O选项,不管是O1还是O2还是O3,设置和恢复帧指针的操作都没有了;
C 有局部变量,但是没有使用,用O1/O2/O3编译,局部变量会被优化掉,并且没有帧指针操作;
D 有局部变量,数量很少,O1有帧指针操作,O2/O3没有;
E 有局部变量,数量较多,O1/O2/O3,都会有帧指针和栈操作;
但是,有一点需要注意,并不是说为了减少局部变量而都定义为全局变量,直白地说,整体考虑,全局变量还不如局部变量效率高呢。
7 在最内部的范围内声明局部变量
不要在函数最外层范围内声明所有局部变量。 如果在最内部的,最小的范围内声明局部变量,则将获得更好的性能。 尤其是在自定义变量的时候。考虑这样的例子; 代码里仅在错误情况下才需要对象“ a”,因此仅仅会在错误检查内调用它。 如果在最外层范围中声明了此参数,则所有函数调用都会产生对象a的创建开销和销毁开销(会调用“ a”的构造函数和析构函数)。
8 减少参数数量
具有大量参数的函数调用成本可能会很高,因为每次调用都会在堆栈上大量推入参数。 出于同样的原因,请避免将完整的结构作为参数传递, 在这种情况下,请使用指针和引用。
x86系统,默认参数通过压栈操作来传递,除非你用regparm,最多可以传递3个参数。从左到右依次使用eax、edx、ecx。x64系统,参数从左到右存放在rdi, rsi, rdx, rcx, r8, r9寄存器,超过六个,从右到左存放在栈上。
9 参数传递和返回值使用引用或指针
通过值传递参数时,完整的参数将复制到堆栈中。这对于常规类型(如整数,指针等)很好。这些类型通常限制为四个字节。当传递更大的类型时,将对象复制到堆栈上的开销可能会过高。在使用类的情况下,为在堆栈上创建的临时副本调用构造函数会产生额外的开销。当函数退出时,析构函数也将被调用。因此,有效的方式是使用引用或指针作为参数传递。这样,您可以节省临时对象创建,复制和销毁的开销。通过用const引用替换值参数传递,可以轻松地执行此优化,而不会对代码产生重大影响。(传递const引用很重要,这样被调用函数中的错误才不会更改参数的实际值)。
将较大的对象作为返回值传递也具有相同的性能问题。在这种情况下,也会创建一个临时返回对象。好像这一段主要针对C++的啊。
10 不使用的返回值就不用返回
是否使用返回值,被调用函数是不知道的,如果它有返回值,不管调用函数是否需要,它都会一直返回。原则就是不需要就不要返回,主要目的还是减少值传递的场景。
阅读(2471) | 评论(0) | 转发(0) |