expr.ml 是 FFTW 代码生成器(genfft)的数据基石。
FFTW 的代码生成的整个流程大概是:
数学算法 (fft.ml) -> 生成表达式树 (expr.ml) -> 化简 (simplify.ml) -> 调度 (schedule.ml) -> 生成 C 代码 (print_c.ml)
在这个流程的每一个环节,expr.ml 里的函数都在起着不可或缺的作用。下面我们逐个函数、逐行地分析它们在整个 FFTW 生命期中的角色。
1. 核心类型定义 (type expr, type assignment)
作用阶段:全过程
这些类型定义是整个 genfft 的"通用货币"。
fft.ml负责制造 这些type expr(比如通过递归分裂生成大量的Plus和Times)。simplify.ml负责修改 这些expr(比如把Times(Num 1, x)变成x)。- schedule.ml 负责分析 这些
expr(看Load节点来决定谁先谁后)。 print_c.ml负责打印 这些expr(把Times打印成*)。
如果没有这个定义,各模块之间就无法交流。
2. hash_float, hash
作用阶段:优化 (公共子表达式消除 CSE)
ocaml
val hash : expr -> int
- 在哪用 :主要被 dag.ml 和
optimizer.ml模块使用。 - 怎么起作用 :
当 genfft 生成巨大的计算图时,里面有无数重复的计算。例如a = x + y;和b = y + x;。
如果不处理,生成的 C 代码就会傻傻地算两次。
编译器会建立一个哈希表(Hashtbl),每生成一个新的expr节点,就先算它的hash,去表里查一下:"我是不是已经算过这个东西了?"- 如果查到了,直接复用之前的变量。
- 没查到,才新建一个。
这就是为什么最后生成的 FFTW 代码极其紧凑,没有任何冗余计算。hash函数必须要保证a+b和b+a哈希值一样(利用加法交换律),才能识别出这种重复。
3. find_vars
作用阶段:调度 (Instruction Scheduling)
ocaml
val find_vars : expr -> Variable.variable list
- 在哪用:主要在 schedule.ml 和 dag.ml。
- 怎么起作用 :
调度器的任务是决定代码的先后顺序。它必须知道:"要计算这行代码t = a + b,我得先知道a和b的值。"
find_vars函数会递归扫描a + b这棵子树,告诉我:"这棵树里用到了变量v1对应的内存地址 和v2对应的内存地址"。
有了这个信息,调度器就能构建依赖图(Dependency DAG) :节点t依赖于节点v1和v2,所以v1, v2必须排在t前面。
4. is_constant, is_known_constant, expr_to_constants, unique_constants
作用阶段:代码生成前准备 (Codegen Prep)
ocaml
val is_constant : expr -> bool
val expr_to_constants : expr -> Number.number list
val unique_constants : Number.number list -> Number.number list
- 在哪用 :
print_c.ml。 - 怎么起作用 :
FFT 算法里不仅有变量,还有大量的数学常数 (比如 cos(2π/N)\cos(2\pi/N)cos(2π/N))。
在生成print_c阶段之前:-
调用
expr_to_constants扫描整棵树,把所有出现的Num 1.234...都抓出来。 -
调用
unique_constants去重。比如0.707106出现了 100 次,只保留一个。 -
最后在 C 文件的开头,生成类似这样的代码:
cstatic const double K1 = 0.70710678...; static const double K2 = 1.41421356...; -
正文里的所有
Num节点会被替换成对K1,K2的引用。这大大减小了代码体积,提高了加载效率。
-
5. to_string, assignment_to_string
作用阶段:调试 (Debugging)
ocaml
val to_string : expr -> string
- 在哪用 :开发者调试
genfft本身时使用。 - 怎么起作用 :
如果你在运行genfft时开启了-dump选项,或者程序崩了,它会把当前的内存里的树打印出来。
比如打印出(:= tmp1 (+ (load input_0) (load input_1)))。
这让维护者知道:"哦,原来在这里它生成了一个错误的加法结构",不然只面对二进制内存是无法调试的。
6. transcendent_to_float, string_of_transcendent
作用阶段:特殊标记处理
- 怎么起作用 :
这些辅助函数主要是为了让hash和to_string等通用函数能够兼容那些"不是数字的数字"(如虚数单位 iii)。
它们就是一种适配层,保证系统不会因为遇到一个特殊标记就抛出异常终止。
总结:数据流向图
| 函数/功能模块 | 输入 | 处理逻辑 | 输出 | 下游使用者 |
|---|---|---|---|---|
type expr |
(数学逻辑) | 构建树结构 | Expression Tree | 所有模块 |
hash |
expr 树 |
计算特征指纹 | int (Hash ID) | optimizer.ml (去重) |
find_vars |
expr 树 |
提取叶子 Load 节点 | 变量列表 | schedule.ml (依赖分析) |
unique_constants |
expr 树 |
提取、收集、去重 Num |
常量列表 | print_c.ml (生成 static const) |
to_string |
expr 树 |
序列化为文本 | Lisp 风格字符串 | 终端/日志 (给人类看) |
这就是这个小小的 expr.ml 如何撬动起庞大的 FFTW 代码生成引擎的。它是整个系统的词汇表 和语法规则。