3. 代码大小优化:去除无用代码、函数内联与宏的权衡、链接脚本精简

各位好,咱们接着聊。RH850 的 Flash 空间,说大不大,说小不小。但做车规级项目,你懂的,功能安全一上,OTA 一加,空间立马捉襟见肘。我见过不少项目,最后阶段因为 ROM 超了 2K,不得不砍功能,那叫一个痛苦。

所以,代码大小优化,不是锦上添花,是雪中送炭。今天咱们就聊聊三个最实用的招数:去除无用代码、函数内联与宏的权衡、链接脚本精简。说白了,就是给你的固件「减减肥」。

3.1 去除无用代码:别让编译器「惯着」你

很多新手写代码,喜欢把整个库都 include 进来。结果呢?编译出来的 .hex 文件,一半都是没用上的函数。RH850 的编译器(比如 GHS 或 IAR)默认会做一定程度的优化,但你不能全指望它。

我个人的习惯是,在项目初期就打开编译器的「去除未使用段」功能。

在 GHS 中,我通常这样设置:

// 链接器选项
--no_keep_secs
--delete_unused_secs

// 编译器选项
-Osize
--no_inline

嗯,这里要注意:--no_inline 是为了配合后面的手动内联,咱们后面会讲。

避坑指南: 我曾经在一个项目中,发现某个中断服务函数明明没被调用,但代码就是去不掉。查了半天,原来是中断向量表里硬编码了它的地址。编译器以为它被「引用」了,所以保留了下来。解决办法?要么用 #pragma 强制优化,要么手动清理向量表。

另外,条件编译也是个大杀器。我建议你多用 #ifdef 来区分不同配置:

#define FEATURE_A_ENABLED   0   // 0: 关闭, 1: 开启

#if FEATURE_A_ENABLED
void feature_a_init(void) {
    // 初始化代码
}
#endif

这样,当 FEATURE_A_ENABLED 为 0 时,整个函数体都不会被编译进最终镜像。干净利落。

小技巧: 在编译完成后,用 map 文件检查一下各个段的尺寸。我习惯用脚本自动解析 map 文件,找出那些「占着茅坑不拉屎」的函数。你想想看,一个 200 字节的函数,如果整个项目周期都没被调用,那它就是白白浪费空间。

3.2 函数内联与宏的权衡:别走极端

这是个老生常谈的话题了。函数内联,能省去调用开销(压栈、跳转、返回),但会增大代码体积。宏呢?本质上是文本替换,没有类型检查,容易出 bug。

我的原则是:

  • 小函数(3-5 行):用 inline 关键字,或者直接写成宏。
  • 中等函数(10-20 行):如果调用次数极少(比如 1-2 次),可以考虑内联。否则,保持原样。
  • 大函数(>20 行):绝对不要内联!否则代码体积会爆炸。

举个例子,一个简单的寄存器读写操作:

// 方式一:内联函数
static inline void gpio_set_high(uint32_t port, uint32_t pin) {
    *(volatile uint32_t *)(port + 0x10) |= (1U << pin);
}

// 方式二:宏
#define GPIO_SET_HIGH(port, pin)   ((*(volatile uint32_t *)((port) + 0x10)) |= (1U << (pin)))

我个人更倾向于用内联函数。为什么?因为宏有副作用。比如 GPIO_SET_HIGH(PORTA, i++),你猜 i 会被自增几次?嗯,这种坑我踩过不止一次。

但是, 在 RH850 上,内联函数并不总是被编译器采纳。编译器有自己的「内联启发式算法」,它觉得不合适,就不会内联。这时候,你可以用 __attribute__((always_inline)) 强制内联:

static inline __attribute__((always_inline)) void gpio_set_high(...) {
    // ...
}

不过,强制内联要慎用。我见过一个项目,工程师把所有函数都加了 always_inline,结果代码体积膨胀了 30%,Flash 直接爆了。你想想看,这得多冤?

核心观点: 函数内联和宏,没有绝对的好坏。关键看场景。小函数、高频调用,用内联;大函数、低频调用,保持原样。宏只适合那些极其简单、且没有副作用的操作。

3.3 链接脚本精简:被忽视的「瘦身」利器

链接脚本(.ld 或 .lcf 文件),很多人觉得它「高深莫测」,不敢动。其实,它恰恰是优化代码大小的关键一环。

链接脚本决定了各个段(.text, .data, .bss)在内存中的布局。 默认的链接脚本,往往为了通用性,做了很多冗余的配置。比如,它可能会为每个输入段都保留一个输出段,哪怕这个段是空的。

我建议你手动精简链接脚本,主要做三件事:

  1. 合并小段: 把多个小的输入段合并到一个输出段中,减少段头开销。
  2. 去除对齐填充: 默认的对齐(比如 4 字节对齐)会浪费空间。对于某些数据,可以改成 1 字节对齐。
  3. 删除未使用的段: 比如 .comment.debug 等调试段,在 Release 版本中完全可以去掉。

举个例子,一个精简后的链接脚本片段:

SECTIONS
{
    .text : ALIGN(4)
    {
        *(.text*)
        *(.rodata*)
        *(.constdata*)
    } > ROM

    .data : ALIGN(4)
    {
        *(.data*)
    } > RAM AT > ROM

    .bss : ALIGN(4)
    {
        *(.bss*)
        *(COMMON)
    } > RAM
}

你看,我把 .text.rodata.constdata 都合并到了 .text 段里。这样,链接器就不会为每个小段单独分配空间了。

警告: 修改链接脚本前,一定要备份!我曾经有一次,把 .data 段的加载地址写错了,结果程序一运行就死机。查了整整一天,才发现是链接脚本的问题。所以,每次修改后,都要仔细检查 map 文件,确认各个段的地址和尺寸是否正确。

另外,利用链接脚本的 KEEP 命令,可以精确控制哪些段必须保留。比如,中断向量表、启动代码等,必须用 KEEP 保护起来,防止被优化掉。

KEEP(*(.intvec))   // 保留中断向量表

嗯,这里要注意:KEEP--delete_unused_secs 是配合使用的。没有 KEEP,编译器可能会把中断向量表也当成「无用代码」给删了。

3.4 实战总结:一个优化案例

我记得有一个项目,初始固件大小是 128KB,目标 Flash 只有 96KB。超了 32KB,怎么办?

我用了上面三招:

  • 去除无用代码:删掉了 3 个未使用的驱动库,省了 8KB。
  • 函数内联与宏的权衡:把 5 个高频调用的小函数改成了内联,省了 2KB(因为减少了调用开销,但代码体积略有增加,不过总体是赚的)。
  • 链接脚本精简:合并段、去除对齐填充,省了 6KB。

最终,固件大小降到了 88KB,还富余了 8KB。你想想看,如果当时没做这些优化,项目可能就要延期了。

最后一个小建议: 优化代码大小,不要等到项目末期才做。从项目一开始,就要有「空间意识」。每次提交代码前,都看一眼编译后的尺寸变化。养成习惯,你会发现,很多问题都能提前避免。

好了,关于代码大小优化,咱们就聊到这儿。下一章,咱们聊聊「执行速度优化:循环展开、查表法与指令调度」。到时候见!