FuncletLayout

该 Pass 是一个 mini Pass 只有几十行

这个 Pass 负责将属于同一个“funclet”(异常处理块)的一组基本块在最终机器函数布局中排序到一起,使得每个 funclet 的代码片段在内存上是连续的。

即→把基于 EH(异常处理)范围划分出的 funclet 按编号排序,保证同一 funclet 的基本块在布局上相邻。

但是 funclet 可能是大家第一次接触这个概念:

在 LLVM 里,“funclet”这个词源自 Windows/ARM 等平台上的“funclet-based EH”(异常处理模型),它把一个函数拆成若干个“小函数”(小片段),每个片段负责处理一类异常控制流——Landing Pad、Catch、Cleanup 等。

  1. “funclet”字面上就是“微小函数”(function + “-let”),它并不是 C/C++ 意义上的独立函数,而是一个在同一个 MachineFunction 下、独立负责某段异常逻辑的基本块集合。
  2. 在 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   (主代码)

这样做的好处是:

  1. 改善指令缓存命中
    异常处理路径往往很少走,如果这些块分散在主代码中,会导致跳转后大量 cache miss。连续布局能让相关代码连续存放,跳转到异常处理时更可能命中指令缓存。
  2. 减少跳转距离
    当从主代码跳到某个 funclet(或从一个 funclet 跳到另一个)时,短距离跳转能用更紧凑的分支编码,也更快;分散布局则可能需要更长的跳转指令或多级跳转。
  3. 简化异常表/元数据生成
    LLVM 在生成 EH 表(如 DWARF 或 Windows 异常目录)时,需要记录每个 funclet 的开始和结束地址。连续的块能让表项变成一个简单的地址区间,数据更紧凑、生成也更高效。
  4. 降低链接器/加载器复杂度
    链接器在做重定位或节合并时,连续相邻的 funclet 节点可以一起处理,不必频繁拆分或合并节,减少重定位条目。
  5. 增强代码维护和分析
    连续布局让剖面工具和调试器在展示异常处理热度或调用图时,更容易把同一 funclet 的执行路径画成一条连贯的线,帮助开发者快速定位和优化。