程序优化——C/C++参数优化

来源:互联网 发布:微信小程序服务器php 编辑:程序博客网 时间:2024/06/05 21:07

C/C++变量优化:

C/C++语言中变量依据其定义的方式不同,其存放位置可以分为寄存器、栈区、堆区和静态存储区三种区域。

(1)寄存器上分配。当函数中定义的局部变量不多,且没有对局部变量的取地址操作时,则会将该变量会分配在寄存器中。当进行运算时,直接读寄存器,速度非常快。

(2)在栈上分配。用户在函数体中定义了较多局部变量后,或对变量进行取地址操作,通过结构体返回值,则相应的变量会放在栈空间。函数执行结束后,这些存储单元自动被释放。一般的情况,由于栈区中数据在函数中都会被重复用到,加载时都能够Cache命中,一个周期内完成,效率很高,但是其分配的内存容量有限。

(3)从静态存储区域分配。在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量、static变量。在ARM9平台,从静态区加载数据到寄存器,一般需要3个周期。如果在循环中,无序访问数据造成Cache不能命中,那么每次都需要3个周期加载,则比较费时。所以尽量让数据顺序访问,提高Cache命中率和访问速度。

(4)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请的内存,程序员自己负责用free或delete释放内存,其访问速度和静态区相同。

clip_image001
建议1:把重复使用的指针型参数拷贝到本地局部变量。

参看下面的代码:

clip_image002

比较左边的C代码,差别在于将step指向的值赋给局部变量temp。程序员一般会认为,A框中列出的 C代码,节约了一个整形变量的存储空间,而且直接用指针进行运算,少了一条赋值语句,速度应该更快。其实不然,将两部分C代码汇编后,再比较右边汇编指令,并没有节约存储空间(原因是变量分配在寄存器中),而且b中汇编代码到在计算每二个表达式时,少用了一条LDR指令。为什么呢?因为编译器不能断言step指针,是否和t1指向同一个地址,即在计算第一个表达式后,其指向值是否发生了改变。为了保证程序正确性,在第二次引用时,只能再次从内存加载step,增加了一个LDR指令周期数。对于这种指针型参数,编译器不能做到优化,如果step处于循环中,情况则更加糟糕。所以只能由程序员自己控制,配合编译器做好优化工作。


建议2:尽量选用32位的数据类型变量。

ARM 指令集支持有符号/无符号的8 位、16 位、32位整型变量。恰当的使用变量类型,不仅可以节省目标代码指令,并且可以提高代码运行效率。在程序编写过程中,应该尽可能地避免使用char、short 型的局部变量,因为操作8位/16位局部变量往往比操作32位变量需要更多指令, 请对比下列函数和它们的汇编代码。

clip_image003

首先比较左边的C代码,唯一的差别是计数器变量i的定义不同。一般认为,采用short类型变量比int节约了两个字节。其实并没有,因为ARM是32位运算(除非处于Thumb模式),寄存器也是32位,并不存在节约了两个字节。在i定义为32整型时,在对应的汇编代码中可以说编译器已经做到极致优化,其将循环结束条件都进行了优化。而在计数器定义为16位整形时,编译器则不但没有优化循环条件,而且还将计数器每次加1之后,利用移位操作对其进行宽度调整,将32位整形其转化为16位整型,增加了两条MOV指令。所以,在变量定义中,尽量使用32位宽度的数据类型,小于32位宽度的变量不但没有节约内存,还增加了很多无用的指令操作。在定义局部变量时,选用32位的数据有利于编译器优化。


(2)、参数传递的优化


传值方式:常量传递、指针传递、引用传递

ARM在函数调用时,如果参数少于四个,则通过R0-R3传递参数。如果多于四个参数,则会将参数从右向左的顺序入栈。所以,在函数设计时,尽量限制函数的参数,不要超过4个,这样可以省去参数入栈操作,提高函数调用效率。函数func_1以传值的方式传递参数,必然会调用CString的拷贝构造函数和析构函数,函数运行过程中不会改变调用者的内容。

函数func_2以指针的方式传递参数,不会调用CString的拷贝构造函数和析构函数,但函数运行过程中,可以对调用者参数所指向的内容进行改变,会造成不安全性。为了保证调用者的值不会被改变,同时也不调用拷贝函数,那么就可以引用传递方式传递参数,在类型前加上const关键字。

(3)、合理使用内联函数

在编译内联函数时,编译器首先对参数和返回值进行检查,确认正确后,内联函数的代码就会直接替换函数调用。这样,就可以省去函数调用的开销,提高函数的执行效率。但是,每一内联函数的调用处都会有其一份拷贝,无疑增大了代码体积,程序加载后消耗更多的内存空间。所以,内联函数是典型的“以空间换时间”的优化策略。如果函数体内代码的执行时间远大于函数调用开销,那么内联的意义就不大了。

所以,使用内联函数时注意以下事项:

(1)将大多数内联函数限制在小型、被频繁调用的函数身上。

(2)内联会导致目标代码体积变大,在空间有限的情况下,不宜使用内联函数。

现在的编译器,都会拒绝将过于复杂的函数内联,自动地取消不值得内联的函数。


(4)、分支优化

条件分支(if语句、switch语句)是编程中经常使用的基本操作,然而在某些时候(如循环)它可能带来严重的性能问题。在ARM上,分支是通过跳转指令B来实现。在ARM7和ARM9中,跳转指令B需要3个指令周期数,是占用指令周期数比较长的一条指令(原因是会清空流水线)。所以,如果在循环中大量使用条件分支,则严重影响程序效率。在XScale中,ARM处理器能对条件分支做出预测。如果分支预测正确,那么跳转指令B只需要花费1个指令周期,而如果预测错误,那么需要增加4个指令周期。这就是分支预测错误的惩罚。

下面将讨论条件分支的一些有效优化方法。

1.把条件分支移动到循环外

先看如下代码,

clip_image008

对于A代码中奇偶模式的随机分支,即使当前最好的CPU也不可能做出好的预测,对于这种情况就要改写成两个for循环,一个处理偶数,一个处理奇数,如代码B。


2.单独处理条件分支。

图像处理算法,经常需要判断边界像素点,进行特殊处理;可以考虑的优化方案是把边界区域和内部区域分开处理;或者条件允许的话,扩大原图像的边界,形成"哨兵"数据,这样访问像素的时候就不用考虑越界的问题了。

3.合并多个条件来减少条件分支

编写代码过程中,常常使用 if((pStr1==0)&&( pStr2==0)&&( pStr3==0)),编译器将生成3个条件跳转指令,而且使分支可预测性大大降低,可以改写为: if((pStr1|pStr2|pStr3)==0) 从而同时改进代码和分支预测准确率。经测试,在GUN ARM编译器的O1优化级别上,能够对于上述语句自动优化。对下面的语句,编译器不能做到优化(因为编译器不能断定b0,b1,b2>=0),会产生三个比较指令和三个跳转指令,这种情形就需要程序员自己优化。

if((b0>=64)||(b1>=64)||(b2>=64))  //b0,b1,b2>=0 

改写为:if((b0|b1|b2)>=64)



yo peace

原创粉丝点击