广告

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

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

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

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

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

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

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

循环

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

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

Pi0ednc

a[i] = b[i]; Pi0ednc

}Pi0ednc

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

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

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

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

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

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

别名

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

char *bufPi0ednc

void clear_buf() Pi0ednc

{Pi0ednc

 int i; Pi0ednc

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

 { Pi0ednc

 buf[i] = 0; Pi0ednc

 } Pi0ednc

}Pi0ednc

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

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

void clear_buf(char *buf)Pi0ednc

Pi0ednc

 int i; Pi0ednc

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

 { Pi0ednc

 buf[i] = 0;Pi0ednc

 } Pi0ednc

}Pi0ednc

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

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

尽量不用后增量和后减量

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

foo = a[i++];Pi0ednc

可以改为Pi0ednc

foo = a[i];Pi0ednc

i = i + 1;Pi0ednc

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

比如以下循环Pi0ednc

i = 0;Pi0ednc

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

 {Pi0ednc

 ... Pi0ednc

}Pi0ednc

应改为Pi0ednc

loop: Pi0ednc

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

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

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

 goto no_loop;Pi0ednc

 ... Pi0ednc

 goto loop; Pi0ednc

no_loop:Pi0ednc

Pi0ednc

loop: Pi0ednc

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

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

 if (temp == 0)Pi0ednc

 goto no_loop;Pi0ednc

 ... Pi0ednc

 goto loop; Pi0ednc

no_loop:Pi0ednc

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

i = 0; Pi0ednc

while (a[i] != 0) Pi0ednc

Pi0ednc

++i; Pi0ednc

... Pi0ednc

}Pi0ednc

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

loop:Pi0ednc

if (a[i] == 0) Pi0ednc

goto no_loop;Pi0ednc

 i = i + 1;Pi0ednc

 ... Pi0ednc

goto loop; Pi0ednc

no_loop:Pi0ednc

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

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

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

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

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

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

结语

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

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

Pi0ednc

Pi0ednc

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

关于更多嵌入式相关的知识,欢迎关注IAR Systems的官方微信公众号。Pi0ednc

  • 微信扫一扫
    一键转发
  • 最前沿的电子设计资讯
    请关注“电子技术设计微信公众号”
  • 波兰网友拆德国产电源插排,内部竟是中国制造?! 本文将会介绍LogiLink LPS262U电源插排(接线板)——包含三个USB端口和两个Schuko插座——的内部结构及其简短测试。
  • 上海微系统所使用石墨烯纳米带研制出世界上最小尺寸的 非易失性相变随机存取存储器(PCRAM)被认为是大数据时代新兴海量存储的有希望的候选者之一。然而,相对较高的编程能量阻碍了 PCRAM 中功耗的进一步降低。利用石墨烯的窄边接触可以有效降低每个电池中相变材料的活性体积,从而实现低功耗运行。
  • 第三代半导体——碳化硅材料之制程与分析 SiC功率电子是加速电动车时代到来的主要动能。以SiC MOSFET取代目前的Si IGBT,不仅能使电力移转时的能源损耗降低80%以上,同时也可让芯片模块尺寸微缩至原本的1/10,达到延长电动车续航里程及缩短充电时间的功效。
  • 苹果iPhone 14 Pro、iPhone 14 Pro Max 将采用更快的 随着iPhone 14系列发布的临近,iPhone 14系列的内存供应商也被曝光。一份由DigiTimes发表的报道显示,今年晚些时候到达的"Pro"型号将配备6GB LPDDR5内存,这比当前一代的iPhone 13 Pro和iPhone 13 Pro Max都有6GB LPDDR4X内存提升了一代。
  • 国际象棋机器人Chessrobot夹断对手手指,意外还是设计缺 据悉,在7月19日的莫斯科国际象棋公开赛期间,一位7岁小男孩疑似因提前走子犯规手,意外被“对手”国际象棋机器人Chessrobot夹住手指,造成指骨骨折,该事件登上了热搜榜。该男孩是莫斯科9岁以下最强的30位棋手之一。
  • M2 Pro 和 M2 Max 或是苹果首款采用台积电3nm 工艺的 M1 Pro 和 M1 Max 最多可配置 10 核 CPU 和 32 核 GPU。借助 M2 Pro 和 M2 Max,Apple 有望突破这一门槛,为这两个领域带来更多的核心数量。目前M2 Pro相关的爆料很少,但据称M2 Max 有12 核 GPU 和 38 核 GPU。12 核 CPU 将包括 10 个性能核心和两个能效核心。
  • 小米12S Ultra游戏性能超越iPhone 13 Pro Max?高通骁龙 高通将骁龙8 Plus Gen 1的量产交给台积电之后,其生产技术带来了许多改进,其中之一是提高了游戏性能。众所周知,Apple 的 A15 Bionic 是目前公认的最快的移动 SoC,但这一认知却被小米 12S Ultra 搭配高通骁龙8 Plus Gen 1所颠覆。
  • 国内外两轮电动车发展趋势与风向 这两年大环境影响下,依靠“生态,低碳环保”理念,电动自行车逆势增长,势头强劲。朱文清指出:“虽然增量市场已经很强大了,但是我们要做好我们自己,把我们的产品做好,把我们的智能化产品做好就可以达到目标。”
  • 世界上尺寸最大的芯片Wafer Scale Engine-2打破了在单 Cerebras公司售价数百万美元的“全球最大AI芯片”Wafer Scale Engine-2又有新消息,在基于单个Wafer Scale Engine-2芯片的CS-2系统上训练了世界上最大的拥有200亿参数的NLP(自然语言处理)人工智能模型。
  • AMD介绍模块化芯片未来,将允许在定制芯片封装中混合非A 据EDN电子技术设计报道,AMD 近日详细介绍了模块化芯片的未来,AMD将允许客户在紧凑的芯片封装中实现多个裸片(也称为chiplet或compute tiles )。
  • 中国“天眼”收到外星信号?不,或许是无线电干扰 日前,《科技日报》报道称“中国天眼”已发现多个“疑似外星文明信号”,引起广泛关注。中国地外文明搜寻首席科学家张同杰教授透露称:“中国天眼”已经发现了几例“来自地球之外可能的科技痕迹和地外文明候选信号”。但经过进一步证实和排除后,他表示这些信号来自无线电干扰……
  • 一种具有触觉感应能力的仿生弹性机器人皮肤 科学家认为,给社交机器人安装类人体皮肤(或触觉传感器),可以实现安全、直观和接触丰富的人机交互。然而,现有的软触觉传感器存在一些缺点,如结构复杂、可扩展性差、易碎,这限制了它们在机器人全身皮肤上的应用。韩国科学技术高等研究院的一组研究人员与麻省理工学院的一位研究人员和斯图加特大学的另一位研究人员合作,开发了一种具有触觉感应能力的仿生弹性机器人皮肤。
广告
热门推荐
广告
广告
EE直播间
在线研讨会
广告
广告
面包芯语
广告
向右滑动:上一篇 向左滑动:下一篇 我知道了