稍微整理了一些书中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指针。
尤其是大的结构体,要用指针传递。
数组,编译器都是按指针传递的,可以不用管。