图优化技术:常量折叠、死代码消除、公共子表达式消除

图优化,说白了就是让编译器帮我们把代码“擦干净”。

我刚开始做芯片设计那会儿,总觉得编译器优化是个黑盒子。后来自己写了一个简单的图编译器,才明白——图优化的本质,就是让计算图更干净、更高效。你想想看,一张乱七八糟的图,映射到硬件上能好到哪去?

今天咱们聊三个最基础的图优化技术:常量折叠死代码消除公共子表达式消除。这三个技术,我几乎在每个项目里都会用到。

常量折叠:把能算的先算掉

常量折叠,名字听着挺唬人,其实意思很简单——编译阶段就把常量表达式算出来

举个例子:

// 优化前
int a = 3 + 5;
int b = a * 2;
int c = b + 1;

// 优化后
int a = 8;
int b = 16;
int c = 17;

你看,3 + 5 这种表达式,运行时算和编译时算,结果一样。那为什么不提前算好呢?

核心思想:将编译时已知的常量表达式,直接替换为计算结果。

我在项目中遇到过一个问题:有个同事写了一段很长的常量计算链,结果综合出来的面积比预期大了30%。后来一查,编译器没做常量折叠——因为常量定义被跨文件引用了。嗯,这里要注意:跨模块的常量定义,很多编译器默认不折叠

常量折叠的好处很明显:

  • 减少运行时计算:硬件不用在运行时算这些固定值
  • 为后续优化铺路:折叠后的常量可能触发更多优化
  • 减小代码体积:指令数少了,存储也省了

我的习惯:在写RTL代码时,能用localparam的地方尽量用,别用表达式。这样综合器更容易做常量折叠。

死代码消除:把没用的扔掉

死代码消除,就是删除那些永远不会被执行到的代码

为什么会生成死代码?原因很多:

  • 条件判断永远为假的分支
  • 定义了但从未使用的变量
  • 冗余的赋值操作

看个例子:

// 优化前
int x = 10;
if (false) {
    x = 20;  // 永远不会执行
}
int y = x + 5;
// y 后面再也没用过

// 优化后
// 整个代码块被删除

我曾经在一个AI加速器项目里,发现编译器生成的中间代码有将近15%是死代码。原因很简单——模板展开时产生了大量无用分支。那次优化之后,芯片面积直接降了12%。

避坑指南:我曾经以为死代码消除很简单,结果有一次把看起来是死代码、实际上是中断服务程序的代码给删了。从那以后我养成了一个习惯——先确认代码的上下文,再决定能不能删

死代码消除对芯片性能的影响:

  • 减少面积:没用的逻辑门不用综合了
  • 降低功耗:没有冗余的翻转活动
  • 提升时序:关键路径变短了

公共子表达式消除:别重复算

公共子表达式消除,英文叫 Common Subexpression Elimination,简称 CSE。

它的核心逻辑就一句话:同一个表达式,算一次就够了

看代码:

// 优化前
int a = b * c + d;
int e = b * c + f;

// 优化后
int tmp = b * c;
int a = tmp + d;
int e = tmp + f;

你想想看,b * c 这个乘法,在硬件里就是一组乘法器。如果重复出现两次,那就得用两组乘法器。用CSE优化后,一组就够了。

我记得有一次做图像处理芯片,核心循环里有三个地方都用了同一个乘加表达式。手动优化后,乘法器从12个降到了8个,面积省了将近20%。

关键点:CSE 不只是减少计算次数,更重要的是减少硬件资源的消耗。在芯片设计中,一个乘法器可能占几百个门,能省一个是一个。

CSE 的适用范围:

表达式类型 能否CSE 说明
算术运算 (+, -, *, /) 可以 最常见,效果最好
逻辑运算 (&, |, ^) 可以 位宽匹配即可
数组索引 有限制 需确认索引不冲突
函数调用 一般不 可能有副作用

我的建议:写代码时,如果同一个表达式出现两次以上,手动提取成临时变量。这样既帮了编译器,也让代码更清晰。

三种优化的关系

这三种优化不是孤立的。它们经常配合使用:

  • 常量折叠之后,可能产生新的死代码
  • 死代码消除之后,公共子表达式可能更容易识别
  • CSE 之后,又可能触发新的常量折叠

说白了,这是一个迭代优化的过程。编译器通常会反复跑这几轮优化,直到没有新的优化机会为止。

下面这张图展示了它们之间的关系:

图优化技术关系图 原始计算图 常量折叠 提前计算常量表达式 死代码消除 删除无用代码 公共子表达式消除 复用相同计算结果 迭代优化 迭代优化 优化后的计算图

从图中可以看到,这三种优化会反复迭代,直到没有新的优化机会。我在实际项目中,一般会让编译器跑3-5轮这样的优化循环。

实际应用中的注意事项

说了这么多理论,来点实际的:

  1. 不要过度依赖编译器:有些优化编译器做不了,比如跨函数的常量传播。写代码时自己注意。
  2. 注意副作用:CSE 不能乱用。如果表达式有副作用(比如修改了全局变量),就不能合并。
  3. 面积和速度的权衡:有时候 CSE 虽然省了面积,但可能引入额外的多路选择器,反而影响时序。要具体分析。
  4. 调试时关掉优化:我习惯在调试阶段关掉这些优化,等功能验证通过后再打开。不然优化后的代码很难单步跟踪。

重要提醒:在芯片设计中,图优化不是越强越好。过度优化可能导致代码可读性变差,甚至引入难以发现的bug。我的原则是——优化要适度,验证要彻底

好了,这三种图优化技术就聊到这儿。它们看似简单,但用好用对,对芯片性能的提升是实实在在的。下次写代码时,不妨想想:这段代码能不能折叠?那句是不是死代码?这个表达式是不是重复算了?


专注资料整理