历史上的今天
返回首页

历史上的今天

今天是:2024年11月09日(星期六)

正在发生

2021年11月09日 | 如何编写有利于编译器优化的代码

2021-11-09 来源:EEWORLD

在嵌入式开发中,代码的体积和运行效率非常重要,代码体积往往和芯片的FLASH、RAM容量对应,程序的运行效率也要求在相应能力的处理器上运行。在大多数情况下,成熟的开发人员都希望降低代码体积、提高代码运行效率,然而具体该怎么做呢?本篇文章将以国际知名编译器厂商IAR Systems的编译器为例,来解答开发人员在实际工作中常常遇到的问题,工程师朋友们可以在IAR编译器上进行实践验证。


对于嵌入式系统,最终代码的体积和效率取决于由编译器生成的可执行代码,而非开发人员编写的源代码;但是源代码的优化,可以帮助编译器生成更加优质的可执行代码。因此,开发人员不仅要从整体效率等因素上去构思源代码体系,也要高度关注编译器的性能和编译优化的便捷性。


有优化功能的编译器可生成既小又快的可执行代码,编译器是通过对源代码的重复转换来实现优化。通常,编译器优化会遵循完善的数学或逻辑理论基础。但是某些编译优化则是通过启发式的方法,经验表明,一些代码转换往往会产生更好的代码,或者开拓出进一步编译优化的空间。


编译优化只有少数情况依赖于编译器的黑科技,大多数时候编写源代码的方式决定了程序是否可以被编译器优化。在某些情况下,即使对源代码做微小改动也会对编译器生成的代码效率产生重大影响。


本文将讲述在编写代码时需要注意的事项,但我们首先应明确一点,我们没有必要尽量减少代码量,因为即使在一个表达式中使用 ?:- 表达式、后增量和逗号表达式来消除副作用,也不会使编译器产生更有效的代码。这只会使你的源代码变得晦涩难懂,难以维护。例如在一个复杂的表达式中间加入一个后增量或赋值,则在读代码的时候很容易被忽略。请尽量用一种易于阅读的风格来编写代码。


循环


下面看似简单的循环会报错吗?


for (i = 0; i != n; ++i) 

a[i] = b[i]; 

}


虽然不会报错,但其中有几点会影响到编译器生成的代码效率。


例如,索引变量的类型应与指针相匹配。


像 a[i] 这样的数组表达式实际上是 *(&a[0]+i*sizeof(a[0]),或者通俗地说:将第 i个元素的偏移量加到 a 的第一个元素的指针上。对于指针运算, 索引表达式的类型最好与指针所指向的类型一致(__far 指针除外,因为其指针所指向的类型和索引表达式的类型不同)。如果索引表达式的类型与指针所指向的类型不匹配,那么在把它与指针相加之前,必须将它强制转换为正确的类型。


如果在应用中,堆栈空间资源(堆栈一般放在RAM中)比代码尺寸资源(代码一般放在ROM或者Flash中)更宝贵,则可以为索引变量选择一个更小的类型来减少堆栈空间的使用,但这往往会牺牲代码尺寸和执行时间(代码尺寸变大,执行时间变慢)。不仅如此,这种转换也会妨碍循环代码的优化。


除上述问题外,我们也要关注循环条件,因为只有在进入循环之前可以计算出迭代次数的情况下,才可以进行循环优化。然而,这项计算工作非常复杂,并非用最终值减去初始值并除以增量那么简单。例如,如果 i 是一个无符号字符,n 是一个整数,而 n 的值是 1000,那么会发生什么情况?答案是变量 i 在达到 1000 之前就会溢出。


虽然程序员肯定不想要一个无限循环,重复地将 256 个元素从 b 复制到 a,但是编译器无法了解程序员的意图。它必须假设最坏的情况,并且不能应用需要在进入循环之前提供行程数的优化。此外,如果最终值是一个变量,您还应该避免在循环条件中使用关系运算符 <= 和 >=。如果循环条件是 i <= n,那么 n 有可能是该类型中可表示的最高值,因此编译器必须假定这是一个潜在的无限循环。


别名


通常,我们不建议使用全局变量。这是因为您可在程序的任何地方修改全局变量,并且程序会因全局变量的值而变化。这就会形成复杂的依赖关系,使人很难理解程序,也很难确定改变全局变量的值会对程序产生怎样的影响。从优化器的角度来看,这种情况更糟糕,因为通过指针的存储就可以改变任意全局变量的值。如果能通过多种方式访问一个变量,这种情况就会被称为别名,而别名使代码更难优化。


char *buf

void clear_buf() 

{

 int i; 

 for (i = 0; i < 128; ++i) 

 { 

 buf[i] = 0; 

 } 

}


尽管程序员知道向 buf 所指向的缓存区进行写操作不会改变这个buf变量本身,但编译器还是不得不做最坏的打算,在循环的每一次迭代中从内存中重新加载 buf。


如果将缓存区的地址作为参数传递,而不是使用全局变量,则可以消除别名:


void clear_buf(char *buf)

 int i; 

 for (i = 0; i < 128; ++i) 

 { 

 buf[i] = 0;

 } 

}


使用这个解决方案后,指针 buf 就不会被通过指针的存储影响。如此一来,指针 buf 在循环中就可以保持不变,其值只需在循环前加载一次即可,而不是在每次迭代时都要重新加载。


然而,如果需要在不共享调用者/被调用者关系的代码段之间传递信息,则直接使用全局变量即可。但是,对于计算密集型任务,尤其是涉及指针操作时,最好使用自动变量。

尽量不用后增量和后减量


在下文中,关于后增量的所有内容也适用于后减量。C 语言中关于后增量语义的标准文本指出:“后缀 ++ 运算符的结果是操作数的值。在得到结果后,操作数的值会递增”。虽然微控制器普遍拥有可在加载或存储操作后增加指针的寻址模式,但其中只有很少能以同样的效率处理其他类型的后增量。为符合标准,编译器必须在执行增量之前将操作数复制到一个临时变量。对于直线代码来说,可以从表达式中取出增量,然后放在表达式之后。

比如以下表达式:


foo = a[i++];

可以改为

foo = a[i];

i = i + 1;


但如果后增量属于 while 循环中的条件,又会发生什么?由于在条件后面没有可以插入增量的地方,因此必须在测试前添加增量。对于这些常见但是又与生成可执行代码效率密切相关的设计,诸如IAR Systems的Embedded Workbench这样的工具都在总结了大量实践后提供了优化方案。


比如以下循环


i = 0;

while (a[i++] != 0)

 {

 ... 

}


应改为


loop: 

 temp = i; /* 保存操作数的值 */

 i = temp + 1; /* 递增操作数 */ 

 if (a[temp] == 0) /* 使用保存的值 */ 

 goto no_loop;

 ... 

 goto loop; 

no_loop:

loop: 

 temp = a[i]; /* 使用操作数的值 */

 i = i + 1; /* 递增操作数 */

 if (temp == 0)

 goto no_loop;

 ... 

 goto loop; 

no_loop:


如果循环后的 i 的值不相关,最好将增量放在循环内。比如以下几乎相同的循环


i = 0; 

while (a[i] != 0) 

++i; 

... 

}


可以在没有临时变量的情况下执行:


loop:

if (a[i] == 0) 

goto no_loop;

 i = i + 1;

 ... 

goto loop; 

no_loop:


优化编译器的开发者们很清楚后增量会使代码编写变得更复杂,尽管我们已尽力去识别这些模式,并尽量消除临时变量,但总有一些情况使我们无法产生有效代码,尤其是遇到比上述更复杂的循环条件时。通常,我们会将一个复杂的表达式分割成若干个更简单的表达式,就像上面的循环条件被分割成一个测试和一个增量那样。


在 C++ 环境中,选择前增量还是后增量的重要性更高。这是因为 operator++ 和 operator-- 都可以以前缀和后缀的形式重载。将运算符作为类对象重载时,虽然没必要模仿基本类型运算符的行为,但也应尽量接近。因此,对于那些可以直观地对对象进行递增和递减的类,例如迭代器,通常会有前缀(operator++() 和 operator--())和后缀形式(operator++(int) 和 operator--(int))。


为了模拟基本类型的前缀 ++ 的行为,operator++() 可以修改对象并返回对修改后对象的引用。那么模拟基本类型的后缀 ++ 的行为会怎样?您还记得吗?“后缀 ++ 运算符的结果是操作数的值。在得到结果后,操作数的值会递增”。就像上面的非直线代码一样,operator++(int) 的实现者必须复制原始对象,修改原始对象,并按值返回副本。由于存在复制操作,因此 operator++(int) 的开销要高于 operator++()。


对于基本类型,如果忽略 i++ 的结果,优化器通常可以消除不必要的复制,但优化器不能将对一个重载运算符的调用变为另一个。如果您出于习惯编写 i++ 而不是 ++i,您就会调用开销更大的增量运算符。


虽然我们一直在反对使用后增量,但不得不承认,后增量在有些情况下还是有用的。如果确实要给一个变量进行后置增量操作,那就继续吧。如果后增量操作和您期望的操作一致,可以使用后增量操作。但请注意,切勿为避免多写一行代码来递增变量,而使用后增量操作。


每当您在循环条件、if 条件、switch 表达式、?:- 表达式或函数调用参数中添加不必要的后增量时,都会使编译器不得不生成更大、更慢的代码。这个清单是不是太长了,记不住?今天就开始培养好的习惯吧!在使用后增量操作前,先问问自己能不能把增量操作作为下一条语句。


结语


当然,软件开发工作并不是只要求开发人员去“将就”编译器,他们与编译器之间的相互协同是快速而高效地完成编程工作的基础之一。此外,从编译器的发展过程来看,它们不仅要跟随技术和语言的演进而迭代和创新,而且还要广泛参考更多的开发习惯,那些历史更悠久、使用更广泛的编译器可以为开发人员带来更高的效率。

因此,在了解了如何编写利于一款优秀编译器优化的代码之后,用户们的工作效率就可以事半功倍。本文中提到的这些原理和tips,也是IAR Systems这样的公司长时间总结的最优实践,而且都可以在该公司的Embedded Workbench中进行验证和探索,在其工具界面中可以查看代码的执行时间和代码尺寸,从而找到最佳解决方案。

 

好的工具除了通用的代码编译优化,还支持高度灵活的自定义优化设置,如IAR Embedded Workbench包含针对运行效率和代码体积的不同优化等级,对于不同的应用需求,还可以设置从整个工程,到每个源代码文件,甚至是每个函数的优化等级,帮助工程师为自己的应用适配出最佳的优化方案。希望此篇文章对于开发人员更深度地了解程序优化有所帮助。


推荐阅读

史海拾趣

Fair Rite公司的发展小趣事

随着全球进入数字化、网络化、智能化时代,电子元器件市场发生了深刻的变化。Fair Rite积极应对市场变化,通过技术创新和产品研发,不断推出适应新需求的产品。例如,公司针对EMF/EMI干扰较严重或容易出现导漏流问题的情况,推出了EMI抑制铁芯线圈(SM-BL系列),帮助客户节省PCB空间并降低干扰信号。同时,Fair Rite还注重满足客户的性价比需求,为每个新产品进行定制化设计和质量验证(遵循ISO9001:2008标准),确保其在不同应用场景下稳定可靠。

这些故事展示了Fair Rite在电子行业中不断发展壮大的历程。通过不断创新、拓展市场和提升品质,Fair Rite已经成为电子行业中的佼佼者之一。

佰鸿(BrtLed)公司的发展小趣事

近年来,佰鸿公司开始将业务触角延伸至再生医学领域。通过多年的努力,公司成功建立了再生医学医疗与健康科技生态圈,并逐步实现了产业集群化。在再生医学领域,佰鸿不仅建立了产业化场地和研发设备,还计划在未来几年内打造国际领先的再生医学产业集群。这一多元化的发展战略,使得佰鸿在电子行业之外,也找到了新的增长点。

GAPTEC Electronic GmbH & Co. KG公司的发展小趣事

背景:2000年代初,随着全球对数字设备需求的激增,Galaxy Microelectronics在硅谷成立。公司创始人是一群来自斯坦福大学的工程师,他们看到了闪存技术在便携式设备中的巨大潜力。

发展:Galaxy Microelectronics的初创团队专注于研发高性能、低功耗的NAND闪存芯片。他们成功开发出了一种创新的电荷撷取闪存(CTF)技术,这项技术有效解决了当时浮栅架构的串扰问题,使得闪存芯片的耐用性和速度大幅提升。这一创新迅速吸引了市场关注,公司开始获得风险投资的支持。

亿宝科技(CNIBAO)公司的发展小趣事

随着环保意识的不断提高,亿宝科技积极响应国家绿色发展的号召。公司引进先进的环保设备和技术,减少生产过程中的污染物排放。同时,亿宝科技还注重产品的环保性能设计,推出了一系列绿色电子产品。这些产品不仅符合国家的环保标准,还赢得了消费者的青睐和认可。在绿色发展的道路上,亿宝科技展现了企业的责任和担当。

BREL International Components公司的发展小趣事

BREL International Components公司自创立之初,就致力于电子元件的技术创新。在早期阶段,公司研发出一款高性能的集成电路,其稳定性和耐用性远超市场同类产品。这一创新为BREL赢得了众多客户的青睐,公司迅速在电子元件市场占据了一席之地。随着技术的不断进步,BREL持续推出创新产品,如低功耗的芯片和高速传输的连接器,进一步巩固了其在行业内的领先地位。

艾为(AWINIC)公司的发展小趣事

2008年,孙洪军牵头创办了上海艾为电子技术股份有限公司。创立之初,艾为就明确了其使命:专注于混合信号、模拟、射频等IC设计,聚焦在手机、可穿戴、智能硬件、IOT等消费电子领域。公司秉持着“客户需求是艾为存在的唯一理由,高素质的团队是艾为的最大财富”的理念,致力于为消费电子市场提供高性能的集成电路解决方案。

问答坊 | AI 解惑

嵌入式

嵌入式系统的好资料…

查看全部问答>

三角波参数测试仪——09年四川

本帖最后由 paulhyde 于 2014-9-15 04:14 编辑 [local]2[/local]“TI”杯时我们三人的作品。 三角波参数测试仪。  …

查看全部问答>

【藏书阁】电路分析 (面向21世纪课程教材)

目录: 第一章 线性电路的复数解法 1.1 电源与元件的概念 1.2 常参量线性电路的复数解法 1.3 一阶RC滤波器 1.4 二级LC滤波器 思考题 习题 第二章 线性电路的s域解法 2.1 拉普拉斯变换 2.2 线性电路的s域解法 2.3 卷积 附录2 拉氏变换 ...…

查看全部问答>

关于可视对讲,一直疑惑的问题

做了这么多年的门禁和可视对讲,做过了好多的产品,现在市场上的产品真的是五花八门, 国内到底谁家, 才是工程商的不二选择......…

查看全部问答>

cycloneIII_3c25的原装开发板到手了-发资料

整快板子上的外设不是很多,但是存储器资源丰富。并预留了一个HSMC接口,可以用来扩展。有三张光盘,一张是介绍开发板的使用,例程,入门指导,另两张是Quartus2的软件,常用库,NIOS2开发资料。先简单介绍到这里,把开发板的资料传上来共享(全部 ...…

查看全部问答>

DSP2407在转速测定中的应用

DSP2407在转速测定中的应用…

查看全部问答>

通过MR16谈谈自己对LED行业的想法--antsin

本人一直在一家电源IC原厂华东区做FAE,07年公司从linear挖了一个团队进入LED驱动行业,我随之也开始进入这个行业。在勤勤勉勉的耕耘3年中,我从学习到与客户分享我的学习经验,很多客户都会跟我讲,王工,从你身上我学到了很多。但这时我告诫自己 ...…

查看全部问答>

如何设置vxworks(主机)的IP地址,和网关?如果设置成功,我能否在windows下 用ping 命令对运行中的vxworks进行网络连接??能ping通吗

如何设置vxworks(主机)的IP地址,和网关????????? 如果设置成功,我能否在windows下 用ping 命令对运行中的vxworks进行网络连接??能ping通吗?? …

查看全部问答>

【MSP430共享】MSP430 电容单触式传感器设计指南

MSP430 电容单触式传感器设计指南,这是官网应用手册,很权威的。 [ 本帖最后由 鑫海宝贝 于 2011-10-12 09:35 编辑 ]…

查看全部问答>

LCD液晶显示

LCD1604驱动模块 /*备注一:LCD1604只含有ASCII码表内字符的字库,无汉字字库,不能自行打点,不便画曲线*/ /*本模板功能一:在任意位置显示阿拉伯数字 */ /*本模板功能二:在任意位置显示字符及字符串 */ #include #include \"lcd1604.h\" /* ...…

查看全部问答>