嵌入式代码提速天花板!11个实用技巧,让CPU跑得比兔子还快

嵌入式代码提速天花板!11个实用技巧,让CPU跑得比兔子还快

写嵌入式代码时你有没有过这种崩溃:逻辑明明没问题,运行起来却慢吞吞?调试半天找不到症结,最后发现竟是代码没“踩对节奏”?其实嵌入式代码的高效运行,不光靠编译器“神助攻”,更要靠自己给代码“松绑”——今天这篇干货,把从算法到变量的优化技巧扒得明明白白,看完直接让你的代码从“龟速爬行”变“光速冲刺”!

1、算法和数据结构:选对“工具”少走弯路

数据结构选不对,再牛的代码也白费。如果要频繁插入、删除数据,链表比数组快得多,毕竟数组是“整块地”,链表是“可拆卸积木”,操作起来灵活太多。

指针和数组的关系就像筷子和勺子——数组直观好理解,指针灵活省空间。大部分编译器下,指针生成的代码更短,执行效率更高,尤其是多维数组,用指针运算代替数组索引,能少做很多复杂的下标计算。

给大家看个直观对比:数组索引是每次循环都要算一遍下标,而指针只要初始时记下地址,之后每次只需要简单增量就行,省时又省力。

2、数据类型:能“瘦”就别“胖”,够用就好

变量类型不是越大越好,就像买衣服,合身比oversize更舒服。能用char(字符型)就别用int(整型),能用int就别用long int(长整型),浮点型float能不用就不用——不是说float不好,而是它会让代码变“笨重”,执行速度直接打折。

这里有个坑要注意:变量赋值不能超范围!C编译器不会提醒你,但运行结果会错得离谱,而且这种bug藏得极深,排查起来能让人头秃。

还有printf参数,尽量用%c、%d、%x这些基础款,少用%ld、%lu这类长整型参数,浮点型%f更是能避就避——同样的功能,用%f会让代码量暴增,运行速度却直线下降,纯属“得不偿失”。

3、运算强度:给CPU“减负”,少做无用功

(1)查表法:提前“算好答案”,循环里直接抄

游戏开发者早就摸透这个技巧:主循环里绝不搞复杂运算,提前把结果做成表格,要用的时候直接查表就行。比如计算阶乘,递归调用又慢又占空间,直接建个阶乘表,想查哪个值直接取,速度快到飞起。如果表格太大,就写个初始化函数,在循环外临时生成,绝不占用循环的“宝贵时间”。

(2)位操作代替求余:1个指令周期搞定

求余运算%看着简单,实则是“隐形耗时大户”——大部分编译器都会调用子程序来完成。但如果是求2的n次方的余数,比如a=a%8,直接改成a=a&7,位操作只要1个指令周期,效率翻倍。

(3)乘法代替平方/立方:硬件乘法器的“主场”

别再用pow(a,2.0)求平方了!在有硬件乘法器的单片机里,a=aa比平方运算快得多,立方运算更是如此——a=pow(a,3.0)改成a=aa*a,效率提升肉眼可见。就算没有硬件乘法器,乘法子程序也比平方子程序更简洁,执行速度更快。

(4)移位代替乘除法:2的n次方运算专属技巧

乘以4不用a=a*4,改成a=a<<2(左移2位);除以4不用b=b/4,改成b=b>>2(右移2位)——移位运算比乘除法子程序生成的代码更高效,CPU执行起来毫无压力。

甚至乘以9这种非2的n次方数,都能拆成移位+加法:a=(a<<3)+a(左移3位相当于乘以8,再加a就是乘以9),运算量直接减半。

(5)避免整数除法:能合并就合并

整数除法是运算里的“慢动作冠军”,尤其连除更耗时。比如m=i/j/k,改成m=i/(j*k),用一次乘法代替一次除法,效率立马提升——不过要注意,乘积不能溢出,不然会得不偿失。

(6)增量/减量操作符:比赋值语句更高效

x=x+1不如x++,x=x-1不如x–。大部分CPU对增、减量操作有专属优化,不用先取内存、再运算、最后存回去,直接一条指令就能完成,既省时间又省代码长度。

(7)复合赋值表达式:编译器的“心头好”

a-=1、a+=1这类复合赋值表达式,能生成高质量的代码,比普通赋值语句更简洁高效,写代码时多用准没错。

(8)提取公共子表达式:避免重复计算

有些浮点表达式里,编译器不会自动提取重复计算的部分,这时候就得手动帮忙。比如e=bc/d、f=b/da,里面都有b/d,直接定义一个临时变量t=b/d,再写成e=ct、f=at,少算一次除法,速度直接提上来。

4、结构体成员:合理布局,不浪费内存

结构体成员的排列顺序,居然也影响效率?没错!编译器会要求长型数据存在偶数地址边界,如果成员顺序混乱,就会出现内存“空洞”,浪费空间还影响速度。

(1)按类型长度排序:长的在前,短的在后

声明结构体时,先放double、long这类长类型,再放char、short这类短类型,避免内存空洞。比如原来把char数组放前面,long和double放后面,会浪费不少空间,调整顺序后再手动填充几个字节,内存利用更充分。

(2)填充成最长类型的整倍数

把结构体整体长度填充成最长成员类型的整倍数,只要第一个成员对齐了,整个结构体就都对齐了,访问速度会更快。

(3)本地变量也按长度排序

本地变量的声明顺序也有讲究,长变量放前面,短变量放后面。这样编译器分配空间时不用额外填充字节,变量能连续存放,自然更高效。

(4)频繁使用的指针参数:拷贝到本地变量

函数里频繁用指针参数指向的值,编译器没法优化,因为它不确定指针之间会不会冲突。所以在函数开头,把指针指向的数据拷贝到本地变量,用完再拷回去,能减少内存带宽占用,速度更快。

5、循环优化:循环是“耗时重灾区”,优化好立竿见影

(1)分解小循环:充分利用指令缓存

如果循环体本身很小,分解循环能提高性能。比如3D转化里的双重循环,直接展开成4条单独的赋值语句,不用反复循环判断,执行速度大幅提升。

(2)循环外提取公共操作:别让循环“做无用功”

循环里那些不依赖循环变量的操作,比如函数调用、数组访问、表达式计算,全放到循环外面。没必要执行多次的操作,集中到初始化函数里,循环只做核心工作。

(3)自减延时比自加更高效

延时函数里,for(i=1000;i>0;i–)比for(i=0;i<1000;i++)更好。几乎所有MCU都有为0转移的指令,自减循环能生成这类指令,代码更短,延时效果却一样。不过要注意,如果循环里用i访问数组,自减可能导致数组超界,得格外小心。

(4)do…while比while更省代码

同样的循环逻辑,do…while编译后生成的代码更短。比如循环1000次,do…while不用先判断条件,直接执行后再判断,少了一次判断指令,效率更高。

(5)循环展开:减少比较次数

循环展开就是把多次循环合并成一次,比如原来循环100次,每次执行1次任务,展开后循环10次,每次执行10次任务,比较指令从100次降到10次,节约不少时间。不过要注意,如果CPU有指令缓存,展开后的代码太大可能导致缓存溢出,反而变慢,得根据情况调整。

(6)循环嵌套:相关操作合并

把两个相关的循环合并成一个,比如初始化二维数组并给对角线赋值,原来要两个循环,合并后一个循环就能完成,少了一次循环初始化和判断,速度更快。

(7)switchcase按频率排序

switch语句如果转化成比较链,会按顺序判断case。所以把发生频率高的case放在前面,能减少判断次数。另外,case用小的连续整数,编译器能转化成跳转表,效率更高。比如统计每个月的天数,31天的月份最多,就把case31放在最前面。

(8)大switch转嵌套switch

如果case特别多,把高频case放在外层switch,低频case放在内层嵌套的switch里,能减少无效比较。甚至可以用函数指针数组代替switch,直接通过索引调用函数,速度更快。

(9)循环转置:从大到小循环

有些CPU对“为0转移”指令优化得特别好,如果循环方向不影响结果,把for(i=1;i<=MAX;i++)改成i=MAX+1;while(–i),执行速度会更快。不过要注意指针操作,避免数组超界。

(10)减少公用代码块的判断

公用代码块里别堆太多if-then-else,复杂的判断会消耗大量时间。如果只是简单判断,偶尔用用还行,复杂逻辑尽量拆分,别让判断拖慢速度。

(11)循环外处理不变的if判断

如果循环里的if条件是常量,不会随循环变化,就把if判断提到循环外面,分成两个单独的循环。这样能避免循环里反复判断,减少对分支预测的依赖,效率更高。

(12)无限循环选for(;;)

while(1)和for(;;)都能实现无限循环,但for(;;)更优。编译后while(1)会有判断和跳转指令,而for(;;)直接跳转,不占用寄存器,指令更少,执行更快。

6、提高CPU并行性:让CPU“多线程”干活

(1)分解依赖代码链:并行执行更高效

把长的、有依赖的代码链,拆成几个无依赖的代码链,让CPU的流水线执行单元并行处理。比如求和数组,原来逐个累加,改成4个变量同时累加,最后再合并结果,能充分利用浮点加法流水线,速度翻倍。

(2)避免读写依赖:用临时变量存数据

数据写入内存后再读取,会有读写依赖,拖慢速度。如果在数组操作中出现这种情况,手动引进临时变量,把数据存在寄存器里,能消除读写依赖,大幅提升性能。

7、循环不变计算:别让循环“重复算账”

循环里那些不随循环变量变化的计算,一定要放到循环外面。比如结构体成员访问,多次访问同一个嵌套结构体的成员,先定义一个临时指针指向它,再通过临时指针访问,能减少重复的地址计算。

还有函数调用,没必要在循环里多次调用的函数,放到循环前的初始化函数里,让循环只专注于核心逻辑。

8、函数优化:让函数调用更“轻便”

(1)Inline函数:用代码替换调用

在C++里,给短函数加Inline关键字,编译器会用函数内部代码替换所有调用,省去调用指令和参数传递的时间。不过这会让程序变长,占用更多ROM,适合频繁调用且代码量少的函数。

(2)不使用的返回值:用void声明

如果函数返回值从来不用,一定要用void明确声明,别让编译器做无用功。

(3)减少函数参数:全局变量谨慎用

全局变量比函数传递参数更高效,省去了参数入栈和出栈的时间。但全局变量会影响程序模块化和重入性,除非必要,别轻易用。

(4)函数要有原型定义

给所有函数写原型定义,能让编译器获得更多优化信息,生成更高效的代码。

(5)多用const常量

const声明的对象,如果不获取它的地址,编译器可能不分配存储空间,让代码更高效。而且const能避免误修改,还能提升代码可读性。

(6)本地函数声明为static

如果函数只在当前文件里用,声明为static,强制内部连接,能帮助编译器优化,比如自动内联。

9、递归:别迷信循环,递归也很高效

很多C程序员觉得递归慢,其实是误解。C编译器对递归调用的优化很到位,只要不传递大量参数,递归比循环更简洁,效率也不差。只有递归导致参数传递瓶颈时,才考虑用循环替代。

10、变量优化:细节里藏着效率

(1)register变量:让变量住进寄存器

声明局部变量时加register关键字,编译器会把变量放进寄存器,而不是堆栈,访问速度大幅提升。函数调用越频繁,效果越明显。

不过要注意,最内层循环尽量不用全局变量和静态变量,编译器没法把它们优化成寄存器变量。另外,别轻易把变量地址传给其他函数,编译器会担心变量被修改,不敢长时间放在寄存器里,频繁读写内存反而变慢。

(2)同时声明多个变量更高效

比起单独声明,同时声明多个变量能让编译器更合理地分配空间,代码更简洁。

(3)变量名越短越好

短变量名不仅输入方便,还能减少代码长度,虽然影响不大,但积少成多,也是一种优化。

(4)循环变量提前声明

在循环开始前声明变量,避免在循环里反复声明,减少内存分配的开销。

11、嵌套if结构:避免无谓判断

如果有多个并列条件要判断,别用长长的if-else-if,拆成嵌套if结构。比如先判断最可能成立的条件,成立就直接执行,不成立再进入下一层判断,能减少很多无谓的比较,提高效率。

最后要提醒一句:优化是门“平衡的艺术”,不是越极致越好。有时候为了速度,可能要牺牲代码可读性;为了空间,可能要放弃一些速度优化。根据项目需求找到平衡点,才能写出既高效又好维护的代码。

这些技巧看似简单,实际用起来效果立竿见影。赶紧把它们用在你的项目里,看看你的嵌入式代码是不是能“脱胎换骨”,跑得比兔子还快!

要不要我帮你整理一份嵌入式代码优化速查手册?把文中核心技巧、代码对比和避坑点汇总成一页纸,方便你开发时快速查阅。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值