博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C代码优化方案 (转载下)
阅读量:4051 次
发布时间:2019-05-25

本文共 6621 字,大约阅读时间需要 22 分钟。

把相关循环放到一个循环里,也会加快速度。
旧代码:
for (i = 0; i < MAX; i++)         
    for (j = 0; j < MAX; j++)
        a[i][j] = 0.0;
    for (i = 0; i < MAX; i++)       
        a[i][i] = 1.0;
 
新代码:
for (i = 0; i < MAX; i++)         
{
    for (j = 0; j < MAX; j++)
        a[i][j] = 0.0;
    a[i][i] = 1.0;                           
}

Switch 可能转化成多种不同算法的代码。其中最常见的是跳转表比较链/。当switch用比较链的方式转化时,编译器会产生if-else-if的嵌套代码,并按照顺序进行比较,匹配时就跳转到满足条件的语句执行。所以可以对case的值依照发生的可能性进行排序,把最有可能的放在第一位,这样可以提高性能。此外,在case中推荐使用小的连续的整数,因为在这种情况下,所有的编译器都可以把switch 转化成跳转表。
不好的代码:
int days_in_month short_months normal_months long_months
。。。。。。
switch (days_in_month)
{
  case 28:
  case 29:
    short_months ++
    break
  case 30:
    normal_months ++
    break
  case 31:
    long_months ++
    break
  default:
    cout << "month has fewer than 28 or more than 31 days" << endl
    break
}
 
推荐的代码:
int days_in_month short_months normal_months long_months
。。。。。。
switch (days_in_month)
{
  case 31:
    long_months ++
    break
  case 30:
    normal_months ++
    break
  case 28:
  case 29:
    short_months ++
    break
  default:
    cout << "month has fewer than 28 or more than 31 days" << endl
    break
 

switch语句中的case标号很多时,为了减少比较的次数,明智的做法是把大switch语句转为嵌套switch语句。把发生频率高的case 标号放在一个switch语句中,并且是嵌套switch语句的最外层,发生相对频率相对低的case标号放在另一个switch语句中。比如,下面的程序段把相对发生频率低的情况放在缺省的case标号内。
pMsg=ReceiveMessage();
        switch (pMsg->type)
        {
        case FREQUENT_MSG1:
        handleFrequentMsg();
        break;
        case FREQUENT_MSG2:
        handleFrequentMsg2();
        break;
        。。。。。。
        case FREQUENT_MSGn:
        handleFrequentMsgn();
        break;
        default:                     //嵌套部分用来处理不经常发生的消息
        switch (pMsg->type)
        {
        case INFREQUENT_MSG1:
        handleInfrequentMsg1();
        break;
        case INFREQUENT_MSG2:
        handleInfrequentMsg2();
        break;
        。。。。。。
        case INFREQUENT_MSGm:
        handleInfrequentMsgm();
        break;
        }
        }
如果switch中每一种情况下都有很多的工作要做,那么把整个switch语句用一个指向函数指针的表来替换会更加有效,比如下面的switch语句,有三种情况:
    enum MsgType{Msg1 Msg2 Msg3}
        switch (ReceiveMessage()
        {
        case Msg1;
        。。。。。。
        case Msg2;
        。。。。。
        case Msg3;
        。。。。。
        }
为了提高执行速度,用下面这段代码来替换这个上面的switch语句。
       
        int handleMsg1(void);
        int handleMsg2(void);
        int handleMsg3(void);
       
        int (*MsgFunction [])()={handleMsg1 handleMsg2 handleMsg3};
       
        status=MsgFunction[ReceiveMessage()]();

有些机器对JNZ(0转移)有特别的指令处理,速度非常快,如果你的循环对方向不敏感,可以由大向小循环。
旧代码:
              for (i = 1; i <= MAX; i++)
               {
                             。。。
                          }
新代码:
              i = MAX+1;
           while (--i)
                         {
                           。。。
                         }
不过千万注意,如果指针操作使用了i值,这种方法可能引起指针越界的严重错误(i = MAX+1;)。当然你可以通过对i做加减运算来纠正,但是这样就起不到加速的作用,除非类似于以下情况:
旧代码:
    char a[MAX+5];
    for (i = 1; i <= MAX; i++)
    {
        *(a+i+4)=0;
    }
新代码:
    i = MAX+1;
    while (--i)
    {
        *(a+i+4)=0;
}

一些公用处理模块,为了满足各种不同的调用需要,往往在内部采用了大量的if-then-else结构,这样很不好,判断语句如果太复杂,会消耗大量的时间的,应该尽量减少公用代码块的使用。(任何情况下,空间优化和时间优化都是对立的--东楼)。当然,如果仅仅是一个(3==x)之类的简单判断,适当使用一下,也还是允许的。记住,优化永远是追求一种平衡,而不是走极端。

要提升循环的性能,减少多余的常量计算非常有用(比如,不随循环变化的计算)。
不好的代码(for()中包含不变的if())
for( i 。。。 )
{
  if( CONSTANT0 )
  {
    DoWork0( i ) // 假设这里不改变CONSTANT0的值
  }
  else
  {
    DoWork1( i ) // 假设这里不改变CONSTANT0的值
  }
}
推荐的代码:
if( CONSTANT0 )
{
  for( i 。。。 )
  {
    DoWork0( i )
  }
}
else
{
  for( i 。。。 )
  {
    DoWork1( i )
  }
如果已经知道if()的值,这样可以避免重复计算。虽然不好的代码中的分支可以简单地预测,但是由于推荐的代码在进入循环前分支已经确定,就可以减少对分支预测的依赖。

在编程中,我们常常需要用到无限循环,常用的两种方法是while (1)  for (;;)。这两种方法效果完全一样,但那一种更好呢?然我们看看它们编译后的代码:
编译前:
while (1)
编译后:
mov eax1
test eaxeax
je foo+23h
jmp foo+18h 
编译前:
for (;;)
编译后:
jmp foo+23h
显然,for (;;)指令少,不占用寄存器,而且没有判断、跳转,比while (1)好。

尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。很多高级语言,包括C++, 并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。需要注意的是,重排序的代码和原来的代码在代码上一致并不等价于计算结果一致,因为浮点操 作缺乏精确度。在一些情况下,这些优化可能导致意料之外的结果。幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。

不好的代码:
double a[100] sum
int i
sum = 0.0f
for (i=0 i<100 i++)
sum += a[i]
 
推荐的代码:
double a[100] sum1 sum2 sum3 sum4 sum
int i
sum1 = sum2 = sum3 = sum4 = 0.0
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段流水线浮点加法,浮点加法的每一个段占用一个时钟周期,保证了最大的资源利用率。

当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。虽然AMD AthlonCPU有 加速读写依赖延迟的硬件,允许在要保存的数据被写入内存前读取出来,但是,如果避免了读写依赖并把数据保存在内部寄存器中,速度会更快。在一段很长的又互 相依赖的代码链中,避免读写依赖显得尤其重要。如果读写依赖发生在操作数组时,许多编译器不能自动优化代码以避免读写依赖。所以推荐程序员手动去消除读写 依赖,举例来说,引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。下面一段代码是一个例子:

不好的代码:
float x[VECLEN] y[VECLEN] z[VECLEN]
。。。。。。
for (unsigned int k = 1 k < VECLEN k ++)
{
  x[k] = x[k-1] + y[k]
}
for (k = 1 k <VECLEN k++)
{
  x[k] = z[k] * (y[k] - x[k-1])
}
推荐的代码:
float x[VECLEN] y[VECLEN] z[VECLEN]
。。。。。。
float t(x[0])
for (unsigned int k = 1 k < VECLEN k ++)
{
  t = t + y[k]
  x[k] = t
}
t = x[0]
for (k = 1 k < VECLEN k ++)
{
  t = z[k] * (y[k] - t)
  x[k] = t

对于一些不需要循环变量参加运算的计算任务可以把它们放到循环外面,现在许多编译器还是能自己干这件事,不过对于中间使用了变量的算式它们就不敢动了,所以很多情况下你还得自己干。对于那些在循环中调用的函数,凡是没必要执行多次的操作通通提出来,放到一个init函数里,循环前调用。另外尽量减少喂食次数,没必要的话尽量不给它传参,需要循环变量的话让它自己建立一个静态循环变量自己累加,速度会快一点。
还有就是结构体访问,东楼的经验,凡是在循环里对一个结构体的两个以上的元素执行了访问,就有必要建立中间变量了(结构这样,那C++的对象呢?想想看),看下面的例子:
旧代码:
    total =
    a->b->c[4]->aardvark +
    a->b->c[4]->baboon +
    a->b->c[4]->cheetah +
    a->b->c[4]->dog;
新代码:
    struct animals * temp = a->b->c[4];
    total =
    temp->aardvark +
    temp->baboon +
    temp->cheetah +
    temp->dog;
一些老的C语言编译器不做聚合优化,而符合ANSI规范的新的编译器可以自动完成这个优化,看例子:
    float a b c d f g;
    。。。
    a = b / c * d;
    f = b * g / c;
这种写法当然要得,但是没有优化
    float a b c d f g;
    。。。
    a = b / c * d;
    f = b / c * g;
如果这么写的话,一个符合ANSI规范的新的编译器可以只计算b/c一次,然后将结果代入第二个式子,节约了一次除法运算。

函数优化

 

C++中,关键字Inline可以被加入到任何函数的声明中。这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用。这样做在两个方面快于函数调用:第一,省去了调用指令需要的执行时间;第二,省去了传递变元和传递过程需要的时间。但是使用这种方法在优化程序速度的同时,程序长度变大了,因此需要更多的ROM使用这种优化在Inline函数频繁调用并且只包含几行代码的时候是最有效的。

函数定义并不知道函数返回值是否被使用,假如返回值从来不会被用到,应该使用void来明确声明函数不返回任何值。

    使用全局变量比函数传递参数更加有效率。这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。然而决定使用全局变量会影响程序的模块化和重入,故要慎重使用。

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

尽可能使用常量(const)C++ 标准规定,如果一个const声明的对象的地址不被获取,允许编译器不对它分配储存空间。这样可以使代码更有效率,而且可以生成更好的代码。

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

与LISP 之类的语言不同,C语言一开始就病态地喜欢用重复代码循环,许多C程序员都是除非算法要求,坚决不用递归。事实上,C编译器们对优化递归调用一点都不反 感,相反,它们还很喜欢干这件事。只有在递归函数需要传递大量参数,可能造成瓶颈的时候,才应该使用循环代码,其他时候,还是用递归好些。

在声明局部变量的时候可以使用register关键字。这就使得编译器把变量放入一个多用途的寄存器中,而不是在堆栈中,合理使用这种方法可以提高执行速度。函数调用越是频繁,越是可能提高代码的速度。
在最内层循环避免使用全局变量和静态变量,除非你能确定它在循环周期中不会动态变化,大多数编译器优化变量都只有一个办法,就是将他们置成寄存器变量,而对于动态变量,它们干脆放弃对整个表达式的优化。尽量避免把一个变量地址传递给另一个函数,虽然这个还很常用。C语言的编译器们总是先假定每一个函数的变量都是内部变量,这是由它的机制决定的,在这种情况下,它们的优化完成得最好。但是,一旦一个变量有可能被别的函数改变,这帮兄弟就再也不敢把变量放到寄存器里了,严重影响速度。看例子:
a = b();
c(&d);

因为d 的地址被c函数使用,有可能被改变,编译器不敢把它长时间的放在寄存器里,一旦运行到c(&d),编译器就把它放回内存,如果在循环里,会造成N 次频繁的在内存和寄存器之间读写d的动作,众所周知,CPU在系统总线上的读写速度慢得很。比如你的赛杨300,CPU主频300,总线速度最多66M, 为了一个总线读,CPU可能要等4-5个周期,得。。得。。得。。想起来都打颤。

在if结构中如果要判断的并列条件较多,最好将它们拆分成多个if结构,然后嵌套在一起,这样可以避免无谓的判断。
说明:
上面的优化方案由王全明收集整理。很多资料来源与网上,出处不祥,在此对所有作者一并致谢!
该方案主要是考虑到在嵌入式开发中对程序执行速度的要求特别高,所以该方案主要是为了优化程序的执行速度
注意:优化是有侧重点的,优化是一门平衡的艺术,它往往要以牺牲程序的可读性或者增加代码长度为代价。
(任何情况下,空间优化和时间优化都是对立的--东楼)

转载地址:http://yapci.baihongyu.com/

你可能感兴趣的文章
autohotkey快捷键显示隐藏文件和文件扩展名
查看>>
Linux中的进程
查看>>
学习python(1)——环境与常识
查看>>
学习设计模式(3)——单例模式和类的成员函数中的静态变量的作用域
查看>>
自然计算时间复杂度杂谈
查看>>
当前主要目标和工作
查看>>
使用 Springboot 对 Kettle 进行调度开发
查看>>
一文看清HBase的使用场景
查看>>
解析zookeeper的工作流程
查看>>
搞定Java面试中的数据结构问题
查看>>
慢慢欣赏linux make uImage流程
查看>>
linux内核学习(7)脱胎换骨解压缩的内核
查看>>
以太网基础知识
查看>>
慢慢欣赏linux 内核模块引用
查看>>
kprobe学习
查看>>
慢慢欣赏linux phy驱动初始化2
查看>>
慢慢欣赏linux CPU占用率学习
查看>>
2020年终总结
查看>>
Homebrew指令集
查看>>
React Native(一):搭建开发环境、出Hello World
查看>>