加快程序运行速度的写法

嵌入式系统 编程技巧之:
加快程序运行速度的写法
影响程序运行速度的因素:
1) 算法:同样的计算结果,不同的算法,速度差距很大;
2) 写法:巧妙的写法,也可以加快运行速度,特别是遇到大循环的时候;
一个程序的运行速度提高,需在平时就采成良好的代码编写习惯,每个地方“一点点”,累积起来才见分晓。
1) 计算:
计算机计算方法中,最快的:++,– , << , >> & | ^ ~
第二级: + – *
第三级: /
第四级: 三角,开方等
有以下优化写法:
a) X = y / a/b/c 应写为: x = y / (abc)

b) 整数乘除2的指数时,应采用:<< , >>
优化成:x >>=n ; x<<=n;

c) 取二进制的余,x = y%16 , 优化为:x = Y&0x0f;

d) x = x +1 ; 应优化成:x++,x–
x++ : 在很多CPU中,仅 1T
   x= x +1 ; 可能需要 3 T

    e) 若程序中存在 除以某个不变数(比如:某个设定的参数,设定之后大量引用),应将这个参数取倒数,改除为乘;
    如: 若程序中需频繁地 / z ,则应优化:
    对于不变的常数: #define  Z  1/z 
       在某个过程中不会变的,则先: Z = 1/z
      在程序中,改成 x = y*Z;

2) 充分利用逻辑运算 0或1 的结果:
If( x > y )
{
Z = y *2;
}
优化成: z = y<<(x>y);

 同理:   m += (X>Y);
          A.0 = (X>Y)
          A.0 |= (X>Y)

CPU运算速度对分支很敏感,这样就避免了分支,大大加快。

3) 充分利用指针寻址:
主要面对数组的同一元素多次引用时,应采取指针法
如: A(i).n =
A(i).m =
………
同一个A(i)多次此用
应优化为:
pA = &A(i);
pA->n =
pA->m =
此用量越大,效果越明显;

4) 在一个逻辑条件语句中常数项永远在左侧。
错误:if( 1 == x ) // 优点:不容易笔误,但相对慢
正确:if( x == 1 ) // 容易笔误成 x = 1 , 特别是初学者
理由:绝大多数CPU都有直接与常数操作的指令

  1. 确保声明和定义是静态的,除非您希望从不同的文件中调用该函数。
    在同一文件函数对其他函数可见,才称之为静态函数。它限制其他访问内部函数,如果我们希望从外界隐藏该函数。现在我们并不需要为内部函数创建头文件,其他看不到该函数。静态声明一个函数的优点包括:
    (1)两个或两个以上具有相同名称的静态函数,可用于在不同的文件。
    (2)编译消耗减少,因为没有外部符号处理。

  2. 尽量使用与CPU位宽相同的数据类型
    如:32位机,尽量使用32位的数据,也有些“伪32位”的CPU,如:TI的C2000系统,期数据基本位宽是16位。

7)、内存对齐
减小内存消耗,加快寻址速度,以32位机为例
struct
{ int16 A;
Float32 C;
Int16 B;
}ABC;
内存中的占用为:
A:16/32
C: 32
B: 16/32
共:3个32位空间
应优化为:
struct
{
int16 A,B;
float32 C;
}ABC;

对齐:
struct
{
int32 A;
int8[9];
}ABC
应优化为:
struct
{
int32 A;
int8[12]; // 多填充3个,实现 32Bit对齐
}ABC
二、减少运算的强度
(1)、查表法
对于耗时较大的运算,特别是频繁使用的运算,应选择“查表法”,如:三角函数的计算,可以按系统的精度要求,在开机启动时,事先准备一个三角函数表,运行时进行查表,要快得多
(2)、平方运算
a=pow(a, 2.0);
可以改为:
a=aa;
说明:在有内置硬件乘法器的单片机中(如51系列),乘法运算比求平方运算快得多,因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR单片机中,如ATMega163中,乘法运算只需2个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。
如果是求3次方,如:
a=pow(a,3。0);
更改为:
a=a
a*a;
则效率的改善更明显。

(3)、使用增量和减量操作符
在使用到加一和减一操作时尽量使用增量和减量操作符,因为增量符语句比赋值语句更快,原因在于对大多数CPU来说,对内存字的增、减量操作不必明显地使用取内存和写内存的指令,比如下面这条语句:
x=x+1;
模仿大多数MCU汇编语言为例,产生的代码类似于:
mov A,x ;把x从内存取出存入累加器A
add A,1 ;累加器A加1
mov x,A ;把新值存回x
如果使用增量操作符,生成的代码如下:
inc x ; x 增1
显然,不用取指令和存指令,增、减量操作执行的速度加快,同时长度也缩短了。

(4)、使用复合赋值表达式
复合赋值表达式(如a-=1及a+=1等)都能够生成高质量的程序代码。

(5)、提取公共的子表达式
通俗地讲:先计算好一些结果,暂放一边备用。
慢代码:
e = b * c / d;
f = b / d * a;
快:
const float t=(b/ d); // 提前算出一个公因子
e = c * t;
f = a * t;

(6)按数据类型的长度排序本地变量

编译器是按变量在源代码中声明的顺序分配内存的(如果自动改变顺序,我估计象我这样的程序猴该跳楼了),把长的变量放在短的变量前面,至少前部分是“内存对齐”的,“空缺”也只发生在后半部,不会“全军履没”。
问题代码:
short Data1,Data2,Data3;
long ……
double x,y,z[5];
char ……
float ……
推荐
double z[5],x,y;
long ……
float ……
short ……
char ……

三、循环优化
(1)、充分分解小的循环
  慢:
for (i = 0; i < 4; i ++)
{
   r[i] = 0;
   for (j = 0; j < 4; j ++)
   {
     r[i] +=M[j][i]*V[j];
   }
}
推荐的代码:
r[0] =M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];
r[1] =M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];
r[2] =M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];
r[3] =M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];
速度快了不是一点点!
1) x[2][3] 是固定的变量地址,直接去取数即可,而:x[i][j] 需经过二重定位,才能找到“位置”
2) 充分利用了CPU的指令缓存。而一个for循环的分支跳转,直接让CPU流水线优势完全无效
所以,是当循环体本身很小的时候,分解循环可以大大提高性能。

(2)、上电慢一点,跑得快一点:
通俗地说:做足准备,方可快跑
对于一些程序中经常要用的参数、数值,在上电初始化时,先将其一一计算好,然后,就可以大大简化后续应用的计算公式,明显加快运行速度。

(3)多次连环判断按发生的概率排序
if( 条件1 ) // 可能性最大 或者 最重要的


elseif( 条件2 )


……

(4)、将大的switch语句转为函数指针
Switch不同的翻译器,可能会转化成多种不同算法的代码。其中最常见的是 跳转表 和 比较链/树。当switch用比较链的方式转化时,编译器会产生if-else-if的嵌套代码,当switch语句中的case标号很多时, 且 case 值为连续整数时,可以巧用函数指针;
事先初始化一个函数指针数组如:
void (pfunCase(void)[]);
初始化:
pfunCase[0] = (void
)&func0();
……
应用:
// 通过函数指针调用,无switch ,快得多
*pfunCaseCaseVel;

(5)提高CPU的并行性,使用并行代码
这需要你对CPU结构充分了解
尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。很多高级语言,包括C++,并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。需要注意的是,重排序的代码和原来的代码在代码上一致并不等价于计算结果一致,因为浮点操作缺乏精确度。在一些情况下,这些优化可能导致意料之外的结果。幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。
慢:
for (i=0; i<100; i++)
{
sum += a[i];
}
快:
for (i = 0; i < 100; i += 4)
{
  sum1 += a[i];
  sum2 += a[i+1];
  sum3 += a[i+2];
  sum4 += a[i+3];
}
sum =(sum4+sum3)+(sum1+sum2);
要注意的是:使用4 路分解是因为这样使用了4段流水线浮点加法,浮点加法的每一个段占用一个时钟周期,保证了最大的资源利用率。

(6)避免没有必要频繁寻址
数组的定位过程,本质就是 数组的起始地址 + 下标偏移量:
慢:
x[k] = x[k] + ……; // 计算之后,更新 x[k] , k 为一个变量
// 接着引用 x[k]
a = x[k] * ……; // 一堆计算
b = x[k] – ……; // 一堆计算
……
快:
n = x[k] = x[k] + ……; // 计算之后,记为:n
// 接着引用 n,避免了频繁的数组定位过程
a = n * ……; // 一堆计算
b = n – ……; // 一堆计算
……

四、函数优化
(1)inline函数
inline函数有点类似于#define 定义,是直接替换插入代码。
普通函数的调用过程:
1) 将当前 PC指针压栈
2) 将传入函数的形参压栈(数据多长,相应的就压多少次)
3) 跳转到函数入口
4) …过程计算……
5) 将计算结果传出
6) 修正堆栈指针(形参部分无用了)到原先的PC指针处
7) 弹栈
inline后,直接将代码嵌入调用的主程序,除“过程计算”外,其它的全省了,如果是“长函数”,无关紧要(10000与10020差别多大?),但若是“短小快”且频繁调用的函数 30 * n 可比 10 * n大得多!
一句话总结:inline 就是以程序空间换时间!所以,仅适用于:
1) 高频度调用
2) 短、小、精快
的子函数

(2)不定义不使用的返回值
减少不必要的形参压栈、结果回传过程。
函数定义并不知道函数返回值是否被使用,假如返回值从来不会被用到,应该使用void来明确声明函数不返回任何值。

(3)减少函数调用参数
使用全局变量比函数传递参数更加有效率。这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。然而决定使用全局变量会影响程序的模块化和重入(中断中调用、多任务操作系统下的调用),故要慎重使用。
全局变量:
内存中定义了一个固定的地址,始终占用内存的一个空间
优点:访问速度快
可以被任何地方调用
缺点:占用内存,不释放
不小心会被其它代码改动,若作为函数的参数,则无法重入(仅引用,不修改除外)

临时变量:
临时占用一下内存,作用完成后,退出内存占用,可以再给其它临时变量使用,即:大家共用一个内存空间,“用完后,我再来用”。
临时变量的空间,都分配在堆栈中。
优点:占用内存少
缺点:访问速度慢

比较两段同样功能的C代码:
代码段1:
int x,y,z ( 全局变量 ),假设内存地址分别固定为100,101,102
………
void f(void)
{
// 对于可以直接寻址的CPU
X = 1; // CPU的操作将数值1直接送入内存地址100处,1 T
Y = 2; // CPU的操作将数值2直接送入内存地址101处,1 T
Z = X + Y; // 从100处取出数,送入Acc ,1 T
与 101处的数相加 1 T
结果送入 102处 1 T
共耗时: 5 T(指令周期)
}

代码段2:
Void f(void)
{
Int x,y,z; // 需要三个内存空间
堆栈指针SP + 3 1T
X对应的地址为:SP-3
Y SP-2
Z: SP-1
x = 1; // 将SP指针的值送入“寻址寄存器” 1T
寻址寄存器中的值 -3 1T
将寻址寄存器指向的位置中送入数值1 1T

y = 2; // 同样 3T

z = x + y; // 将SP指针的值送入 寻址寄存器 1T
寻址寄存器中的值 -3 1T
将寻址寄存器指向的位置中的值送入Acc 1T

             将SP指针的值送入 寻址寄存器              1T
              寻址寄存器中的值 -2                       1T
              将寻址寄存器指向的位置中的值与Acc相加    1T

将SP指针的值送入 寻址寄存器 1T
寻址寄存器中的值 -1 1T
将Acc中的值送入寻址寄存器指向的位置 1T

紧接着,Delete x,y,z; 释放内存,SP – 3 1T
函数返回,SP回原,内存释放,这个空间,可以让给其它函数调用。

   // 总共: 17 T

紧接着,函数返回,SP回原,内存释放,这个空间,可以让给其它函数调用。

}

  1. 加快函数参数的调用
    对于非内部数据类型(如:CPU标准的int)的参数而言,象void Func(struct BigStruct VarA) 这样声明的函数注定效率比较低。因为首先需在堆栈中重构一个 struct 临时对象用于复制参数VarA,这需要时间,更要命的是:这个数据类型是一套大型结构体,不仅慢,而且,巨大的结构体可能直接导致小小的堆栈溢出,还让你的Bug难以再现!
    因此,尽量将函数优化为:Func(A* a),这样,永远仅复制一个指针变量;
    这种写法有一个问题:在函数体内不小心改写了 *a 的值,所以,需要用 “const”进行保护,改写为:
    void Func( A const *a); ( a 的值可以改,但:a 的值无法改动)
    另一种写法:
    void Func( A
    const a); ( a 不能改,a 的内容可以改 ),此写法,一般用于将结果回传给一个接收变量,如:void GetDateTime(DateTime_Def Const pDateTime);函数的功能就是将相关的值送入结构变量 *pDateTime, pDataTime的值不可变,这样,你永远不会将数据“送错地方”。
    助你理解两者的区别:const 关键字后的不可变。

如果是C++,建议采用引用的方式,速度更快: void Func( A const &a);(嵌入式系统不推荐C++)

  1. 所有函数都应该有原型定义
    一般来说,所有函数都应该有原型定义。原型定义可以传达给编译器更多的可能用于优化的信息。

(5)少用常量(const)
除了确实需要一个常量数组(或表)之外,应采用 #define, 不要用 const , 因为const 也是变量的一种,也需要分配内存,而嵌入式系统内存本就短缺。

(6)把本地函数声明为静态的(static)
  如果一个函数只在实现它的文件中被使用,把它声明为静态的(static)以强制使用内部连接。否则,默认的情况下会把函数定义为外部连接。这样可能会影响某些编译器的优化——比如,自动内联。

7)减少“参数合法性检查”
一般的,为了程序的“健壮”,函数的入口处,首要检查“参数的合法性”,但检查工作耗时,因此,为了提高效率,应将除了提供对外接口的函数外,将其它仅供内部使用的子函数加上 static 关键字,这样,这些子函数对外部不可见。
“参数合法性检查”仅在对外接口的函数进行,内部子函数就不再“设防”,你自己小心一点就行了。

8)按速度要求对你的功能函数分类:
程序的功能要求、调用的频繁度,总是有区别的,以下程序对效率一定不敏感:
 上电初始化(启动之后,不会再被调用,或者调用的可能性极低)
 意外处理(经常发生的,就不是“意外”)
 外设的初始化及重新初始化(一种外设,定义的何种工作模式,轻易不会变)
 其它效率无关紧要的函数;
若系统的高速Cache区太小,无法装下整个程序时,应学会手动分配内存,将这些“低速函数、数据”搬到“远处”运行,让宝贵的Cache留给“高效函数”和“高速数据”。

五、变量
(1)register变量
在声明局部变量的时候可以使用register关键字。这就使得编译器把变量放入一个多用途的寄存器中,而不是在堆栈中,合理使用这种方法可以提高执行速度。函数调用越是频繁,越是可能提高代码的速度。
(2)、同时声明多个变量优于单独声明变量(内存对齐)
(3)、在循环开始前声明变量

七、开启编译器优化:
开启编译优化,不仅会对代码的分支、常量、表达式进行优化,还会尝试更多寄存器和指令级的优化,编译期间会占用更多的内存和编译时间,但对于运行效率会有很大提升。当然也会带来一些麻烦,它会改变代码结构,比如对对分支的合并和消除,对公用子表达式的消除,对循环内load/store操作的替换和更改等,都会使目标代码的执行顺序变得面目全非,导致调试信息严重不足。
未开优化时,编译后的汇编代码,本猴看得明明白白,优化后,立马“天书”。
一般的,调试时,不优化,发布时开启。

    原文作者:笨笨的猴
    原文地址: https://blog.csdn.net/qq_36994831/article/details/83042245
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞