如何编写有利于编译器优化的代码

发布者:EE小广播最新更新时间:2021-11-09 来源: EEWORLD作者: IAR Systems关键字:编译器  代码  嵌入式  IAR  低代码 手机看文章 扫描二维码
随时随地手机看文章

嵌入式开发中,代码的体积和运行效率非常重要,代码体积往往和芯片的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包含针对运行效率和代码体积的不同优化等级,对于不同的应用需求,还可以设置从整个工程,到每个源代码文件,甚至是每个函数的优化等级,帮助工程师为自己的应用适配出最佳的优化方案。希望此篇文章对于开发人员更深度地了解程序优化有所帮助。


关键字:编译器  代码  嵌入式  IAR  低代码 引用地址:如何编写有利于编译器优化的代码

上一篇:意法半导体更新TouchGFX软件,增加视频功能丰富STM32用户体验
下一篇:Socionext为下一代云标签开发LSI加速物流数字化转型

推荐阅读最新更新时间:2024-11-10 02:40

DDS技术的原理介绍及用其和单片机进行嵌入式信号源的设计
信号源是指收音头、高频头、录音卡座、录像卡座等器件。微机及辅助设备完成信号的提取、数模转换、数字信号处理等功能。信号源是雷达系统的重要组成部分。雷达系统常常要求信号源稳定、可靠、易于实现、具有预失真功能,信号的产生及信号参数的改变简单、灵活。信号发生器又称信号源或振荡器,是用来产生各种电子信号的仪器,在生产实践和科技领域中有着广泛的应用。传统的RC或LC自激振荡器方式的信号源组成较繁杂,调试较困难,不易实现程控,已不能适应新的要求;而由采用专用IC芯片构成的信号发生器。另外,采用FPGA+D/A可实现正弦信号发生器的设计,同时可实现频率步进调节;但当输出高频信号时,需要高速D/A来配合工作,成本较高。频率合成与锁相技术的应用,可获
[单片机]
DDS技术的原理介绍及用其和单片机进行<font color='red'>嵌入式</font>信号源的设计
Microchip发布面向VS Code® 的MPLAB® 扩展早期访问版本
赋能设计人员在流行集成开发环境中使用Microchip开发工具 此次发布是公司长期计划的第一步,旨在扩大产品组合、更好地服务VS Code 生态系统开发人员 为充分利用Microsoft® Visual Studio® Code (VS Code®) 的多功能性,Microchip Technology(微芯科技公司)今日发布面向VS Code 的 MPLAB® 扩展(MPLAB® Extensions)早期访问版本。 此次发布为嵌入式设计人员提供了将项目从MPLAB X集成开发环境(IDE) 导入VS Code的工具,同时仍可使用Microchip的调试和编程支持。这一举措是Microchip长期战略的一部分,旨在扩展其
[嵌入式]
Microchip发布面向VS Code® 的MPLAB® 扩展早期访问版本
嵌入式实现地铁杂散电流监测装置
  1 地铁杂散电流产生原理   地铁牵引供电一般为直流供电,而当直流大电流沿地面敷设的轨道流动时,直流电流除了在轨道中流动外,还会从轨道泄漏到大地,在大地中的各种金属物体上流动,然后再回到电源系统。这部分泄漏出来的电流称为杂散电流,在地铁工程中又称为迷流,如图0所示。由于杂散电流对埋入地下金属产生腐蚀作用,就可能使得某些地方的地下金属在自然腐蚀的同时又受到严重的杂散电流电腐蚀作用,导致地铁电化腐蚀速度加快。   2 实验室模拟装置的设计   由于地下铁道的特殊环境, 理论上和实际中都难以在现场进行实验, 因此这类课题的研究和实验, 多数情况下往往要在实验室里进行,图1是自行设计的一个实验室模拟地下铁道杂散电流的产生和对地下金
[单片机]
<font color='red'>嵌入式</font>实现地铁杂散电流监测装置
如何优化AVR C语言代码(程序员必读)
1、选择合适的算法和 计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找 法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大 提高程序执行的效率。.选择一种合适的数据结构也很重要,比如你在一堆随机存 放的数中使用了大量的插入和删除指令,那使用链表要快得多。 数组与指针语句具有十分密码的关系,一般来说,指针比较灵活简洁,而数组则比 较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短, 执行效率更高。但是在Keil中则相反,使用数组比使用的指针生成的代码更短。。 3、使用尽量小的数据类型 能够使用字符型(char)定义的变量,就不要使用 整型(int)变量来定义;能够使用
[单片机]
嵌入式DSP访问片外SDRAM的低功耗设计研究
DSP有限的片内存储器容量往往使得设计人员感到捉襟见肘,特别是在数字图像处理、语音处理等应用场合,需要有高速大容量存储空间的强力支持。因此,需要外接存储器来扩展DSP的存储空间。 在基于DSP的嵌入式应用中,存储器系统逐渐成为功耗的主要来源。例如Micron公司的MT48LC2Mx32B2-5芯片,在读写时功耗最大可以到达924 mW,而大部分DSP的内核功耗远远小于这个数值。如TI的TMS320C55x系列的内核功耗仅仅为0.05 mW/MIPS。所以说,优化存储系统的功耗是嵌入式DSP极其重要的设计目标。本文主要以访问外部SDRAM为例来说明降低外部存储系统功耗的设计方法。 1 SDRAM功耗来源 SDRAM内部一般分为多个存
[嵌入式]
<font color='red'>嵌入式</font>DSP访问片外SDRAM的低功耗设计研究
Stm32采用环形缓冲区接收rk3588的数据代码
```c #include stm32f10x.h #include usart.h #include ring_buffer.h #define BUFFER_SIZE 128 uint8_t buffer ; // 定义一个大小为128的缓冲区 ring_buffer_t ring_buffer; // 定义一个环形缓冲区结构体 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) // 判断是否接收到数据 { uint8_t data = USART_ReceiveData(USART1); // 读取接收
[单片机]
你愿意在工位上熬夜写代码还是放松的创意
在很多人印象中,茶馆遍街、麻将入耳的成都是个追求安逸的地方。但对在城南的互联网创业公司和科技创业公司中工作的人来说,安逸这个词,真与他们不搭。记者在晚上9、10点去拜访创业公司,经常看到程序猿、攻城狮都还挂在加班这根弦上。 成都与北上广深比起来,一样的繁华、一样的拥堵、一样的加班,一样的,拼起来不要命。 但可以选择的是,你是愿意在狭小工位上熬夜写代码,还是愿意在公园式的工作环境中让创意舒展? 蓉漂的新落脚地? 什么样的环境最吸引科创企业?不妨拿全球科创最活跃的美国硅谷地区举例。 今年3月,美国硅谷城市群市长组团造访成都,共建“成都-硅谷科技金融中心”。门洛帕克市(Menlo Park)的市长克斯汀凯思(K
[半导体设计/制造]
你愿意在工位上熬夜写<font color='red'>代码</font>还是放松的创意
GD32代码移植STM32(一)
GAIWEI例子:GD32F103移植STM32F103 使用相同FLASH和管脚数量相同的芯片,例如GDF103C8T6移植STM32F103C8T6程序。虽然两个款芯片的寄存器地址以及架构基本相同。但是需要注意的是GD32F10x主频是108兆,但是STM32F10x主频是72兆。所以需要针对以RCC时钟进行修改。 1.先将芯片的选项进行修改:查找对应芯片。 2.将STM32的启动文件替换成GD的启动文件。 3.修改时钟相关配置。 打开stm32f10x.h文件,#define HSE_STARTUP_TIMEOUT ((uint16_t)0x0500) /*! Time out for HSE start up
[单片机]
GD32<font color='red'>代码</font>移植STM32(一)
小广播
最新嵌入式文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved