《嵌入式C编程实战》代码优化

        稍微整理了一些书中6.3章节说得一些代码优化技术,有些我自己的理解和有一些我觉得根本没必要的就删掉了。

代码优化原则

        代码的正确性大于代码的执行效率!!!如果代码执行结果都不对,再快毫无意义。

如果保证代码的正确性(个人经验)

        1、易读,逻辑通畅不晦涩,大道至简。

        2、减少使用二维以上数组。

        3、不用goto,不用递归,业务逻辑不掺杂平台相关的代码(不好移植)。

        4、禁止编译器优化。

        5、用设计隔离变更,尽量保证二进制级别的稳定性。

        6、代码评审,接受专家的建议。

        7、提供完整的测试建议给测试人员。

        8、软件测试应该以发现bug为荣,而不是要力求证明软件的稳定性!

        9、lint代码走查。

        10、循环变量越界检查、确认循环退出条件,确保不会造成死循环。

        11、通信带宽使用率过高风险、栈使用率和溢出风险、CPU使用率过高风险。

        12、平滑采样结果,去除毛刺。

        13、变量溢出。

        14、代码注释尽可能详尽的表达代码的意思,来龙去脉。

        15、异常的风险分析,单一故障处理与报警。

        16、敏捷开发。优化代码采用增量式重构(每次重构一小部分)。

1、减小变量的作用域(全局变量的教训)

如下:

这里把errcode放到了for循环内部。

作用就是优化内存占用,errcode在for循环外就被释放了。

书上并没有提到,是我个人的开发经验。

        1、全局变量是程序代码混乱的源头!

        2、全局变量很可能是出错的罪魁祸首。

        3、如果要用全局变量,尽量使用static,然后用一个接口函数去获取,可以方便对全局变量进行约束。

        4、不同优先级的中断,如果要交换数据,一定要注意数据读写的原子性,措施就是做中断屏蔽或做变量隔离,一定不要读写同一个全局变量。

血与类的教训实例:

        产品软件代码,有两个中断:

        中断A通过SPI接口采集传感器两个字节的数据,存放到全局变量unsigned short gValue里面。

        中断B读取全局变量gValue,然后用这个全局变量计算一些结果。

        中断B的优先级高于中断A。

        可想而知,使用全局变量的结局并不会太好!

        在A中断的采样中,gValue被分为两次赋值,先赋值低字节,再赋值高字节。

        

unsigned short gValue = 0;

// 从SPI接口获取数据
void getValue(unsigned short *v)
{
    #define spi_byte1 0x00000010    // 第一个SPI字节地址
    #define spi_byte2 0x00000011    // 第二个SPI字节地址
    // 下面这句话不是原子操作,可能被中断B打断,导致只赋值了一个字节
    *v = (*((unsigned char *)spi_byte2) << 8 | (*(unsigned char *)spi_byte1));
}

// 中断A,优先级1
void zhongduanA()
{
    getValue(&gValue);
}

// 中断B,优先级2 > 优先级1
void zhongduanB()
{
    callAlg(gValue); // 调用算法,这里的gValue可能只被赋值了一个字节
}

        由于中断B的优先级高于中断A,所以,会碰到偶尔先赋值完低字节,全局变量就被中断B取走的情况!而且这是偶发bug!能稳定复现的问题都是问题,真正难搞得是偶发问题,这就是全局变量带来的偶发问题。

        一个对代码影响最小的更改方案(虽然还有全局变量):

        

void zhongduanA()
{
    // 增加一个局部变量,对该变量的赋值不是原子的,但是不会被中断B使用。
    static unsigned short mValue;
    getValue(&mValue);
    gValue = mValue; // 这个赋值是一条指令,原子操作
}

       

2、使用与指针大小相同的变量

        因为假如你使用的变量类型在目标机器上不支持,又涉及到一个类型转换的操作。而如果你使用与指针大小相同(即目标机器的位数)的变量,就无需转换了。

3、使用无符号变量

如果不需要表示负数,尽量使用无符号变量。

4、避免使用volatile

        如果一个变量被声明为valatile,编译器便不会对这个变量做任何优化。如果没有这个关键字,编译器可能会将其放在cache中,而不会把结果刷新到实际内存区域。

       我认为,嵌入式程序员应该避免使用-O的优化,适当使用volatile(嵌入式软件有些情况,必须要volatile)。-O优化会给代码带来很多意料之外的问题!嵌入式程序需要的是稳定且符合预期,如果你不能彻底掌握编译器的优化原则,那你还是不要铤而走险!千万不要产品出问题了,然后回过头来查问题,查出来是优化的问题,然后发出“哦,原来是这里被优化了,这么难发现的bug都被我发现了,我真厉害”的感慨,但是如果你一开始就禁止优化,这个问题根本不会出现!代码的一切行为都是那么显而易见。不要为了小性能优化,付出沉痛的代价,如果真是为了性能或内存空间,应该在项目MCU选型的时候就要预留更多的CPU算力和内存!

5、减少变量类型不匹配

与2类似,都是涉及到转换扩展的操作。

6、频繁更新的变量声明为寄存器变量

register int value1; 我记得register只是给编译器一个建议,具体还得看编译器咋编。

register和volatile就是两个完全相反的关键字:

volatile:强制刷新到内存。

register:尽量使用CPU 寄存器。

7、使用临时变量

我觉得这个影响可能不大,如果没有很大的循环的话。

假设有一个函数

// 以下代码对arr取值解引用了1次。
int myadd(char *arr)
{
    if (arr[1] >= 12) // 对arr取址解引用
    {
        retrun arr[1]; // 对arr取址解引用
    }
    else
    {
        retrun arr[1]+ 1; // 对arr取址解引用
    }
}

更改后:

// 以下代码对arr取值解引用了1次。
int myadd(char *arr)
{
    char temp = arr[1];  // 对arr取址解引用
    if (temp  >= 12)
    {
        retrun temp;
    }
    else
    {
        retrun temp  + 1;
    }
}

8、结构体对齐

将结构体中占用地址空间最大的成员放在第一位,以确保达到最佳的内存对齐,减少访问时间。

        这个一般编译器都会帮你对齐,但是一般我们最好手动对齐,防止出现意外的问题,C程序员就是要一切都掌握在自己手中!

9、结构体成员分组

        将经常访问到的数据放在一起,这样可以提高cache、虚拟内存的命中率。这个有点用。

 10、使用bit节省内存

用过的人一看就懂,下面的mystruct一共占4个字节,8是8bit的意思。

struct mystruct{

        unsigned int v1:8;

        unsigned int v2:8;

        unsigned int v3:8;

        unsigned int v4:8;

}

11、使用static来修饰函数

        这个一定要记得,static可以限制程序的自由度,C语言程序的缺陷就是太自由了,程序员想调哪个函数就调哪个函数,这也意味着bug的扩展!不必要的接口绝对不暴露给外部使用!性能都是次要的。

12、使用const修饰函数参数

        如果明确变量在函数内部不会被修改,增加const修饰可以增加参数被放置到CPU寄存器的机会,加速函数的调用。

13、 使用自减运算符--替代自增运算符++

        作者说主要原因是大部分目标处理器的指令集提供了自建branch-if-zero(零分支)类型的运算功能。但是我觉得大可不必,如果说为了这点性能,牺牲了代码的可读性,那将得不偿失!        

14、循环合并

合并循环次数一样的操作。

15、使用switch替代if(存疑)

        实测switch的代码比if生成的汇编代码要多。基本上可以不用管考虑这个问题,现代处理器和编译器不差这点性能,还是那句话,可读性第一,要易于理解!

// 测试函数
unsigned short getValue(unsigned short c1)
{
    unsigned short ret = 0;
    /*
    if (c1 == 123)
    {
        ret = 1;
    }
    else if (c1 == 456)
    {
        ret = 2;
    }
    else if (c1 == 789)
    {
        ret = 3;
    }*/


    switch (c1)
    {
        case 123:
            ret = 1;
            break;
        case 456:
            ret = 2;
            break;
        case 789:
            ret = 3;
            break;
    }

    return ret;
}

 16、增加指针传递而不是值传递

避免函数调用的时候内存拷贝,如果不放心内容被修改,可以声明为const指针。

尤其是大的结构体,要用指针传递。

数组,编译器都是按指针传递的,可以不用管。

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值