后端Pass简介——FuncletLayout
FuncletLayout
该 Pass 是一个 mini Pass 只有几十行
这个 Pass 负责将属于同一个“funclet”(异常处理块)的一组基本块在最终机器函数布局中排序到一起,使得每个 funclet 的代码片段在内存上是连续的。
即→把基于 EH(异常处理)范围划分出的 funclet 按编号排序,保证同一 funclet 的基本块在布局上相邻。
但是 funclet 可能是大家第一次接触这个概念:
在 LLVM 里,“funclet”这个词源自 Windows/ARM 等平台上的“funclet-based EH”(异常处理模型),它把一个函数拆成若干个“小函数”(小片段),每个片段负责处理一类异常控制流——Landing Pad、Catch、Cleanup 等。
- “funclet”字面上就是“微小函数”(function + “-let”),它并不是 C/C++ 意义上的独立函数,而是一个在同一个 MachineFunction 下、独立负责某段异常逻辑的基本块集合。
- 在 Windows SEH(结构化异常处理)或 ARM EHABI 上,异常处理代码(例如 catch 块、析构清理块)必须独立出来、与普通代码分开,这些独立的块 LLVM 就叫它们 “funclet”。
在使用 funclet-based 异常处理模型的目标上,异常处理逻辑被拆成多个“funclet”——每个 funclet 对应一段需要单独处理的代码。FuncletLayout 通过查询 EH 范围(getEHScopeMembership)得到每个基本块所属的 funclet ID,然后对整个 MachineFunction 的基本块列表进行排序,按 funclet ID 升序排列。
简单的话就是说:
void foo() {
try {
mayThrow(); // 普通基本块:B0
doWork(); // 普通基本块:B1
} catch (int e) {
handleInt(e); // catch-int funclet:F1 中的块 C1
} catch (...) {
handleAll(); // catch-all funclet:F2 中的块 C2
}
cleanup(); // 普通基本块:B2
}
最好把正常执行(异常处理外)的代码集中起来排布:
B0 → funclet 0 (主代码)
B1 → funclet 0 (主代码)
C1 → funclet 1 (int-catch)
C2 → funclet 2 (catch-all)
B2 → funclet 0 (主代码)
这样做的好处是:
- 改善指令缓存命中
异常处理路径往往很少走,如果这些块分散在主代码中,会导致跳转后大量 cache miss。连续布局能让相关代码连续存放,跳转到异常处理时更可能命中指令缓存。 - 减少跳转距离
当从主代码跳到某个 funclet(或从一个 funclet 跳到另一个)时,短距离跳转能用更紧凑的分支编码,也更快;分散布局则可能需要更长的跳转指令或多级跳转。 - 简化异常表/元数据生成
LLVM 在生成 EH 表(如 DWARF 或 Windows 异常目录)时,需要记录每个 funclet 的开始和结束地址。连续的块能让表项变成一个简单的地址区间,数据更紧凑、生成也更高效。 - 降低链接器/加载器复杂度
链接器在做重定位或节合并时,连续相邻的 funclet 节点可以一起处理,不必频繁拆分或合并节,减少重定位条目。 - 增强代码维护和分析
连续布局让剖面工具和调试器在展示异常处理热度或调用图时,更容易把同一 funclet 的执行路径画成一条连贯的线,帮助开发者快速定位和优化。
评论