llvm_history

第3章:Pass 基础设施 - 模块化优化的艺术

本章大纲

3.1 Pass Manager 的设计理念

3.2 从 Legacy PM 到 New PM:架构重构的必要性

3.3 Pass 依赖性和调度算法

3.4 分析信息的缓存与失效机制

3.5 优化 Pass 的组合策略:-O0 到 -O3 的演进

3.6 高级话题:Coroutine Passes 的设计挑战与 CO-RE 技术

3.7 核心人物与关键事件


开篇:理解 Pass 基础设施的重要性

LLVM 的 Pass 基础设施是整个编译器优化系统的核心骨架。与传统编译器将优化过程硬编码在固定流程中不同,LLVM 通过 Pass 机制实现了优化的模块化、可组合和可扩展。这种设计不仅使得新优化的添加变得简单,更重要的是建立了一套分析和转换相互协作的框架。本章将深入探讨 Pass 系统从早期 Legacy Pass Manager 到现代 New Pass Manager 的演化历程,理解其背后的设计权衡和工程挑战。

3.1 Pass Manager 的设计理念

Pass 的概念与分类

在 LLVM 中,Pass 是对 IR 进行分析或转换的基本单元。每个 Pass 都是一个独立的组件,具有明确定义的输入输出接口。这种设计源于编译器构造中的一个核心观察:大多数优化都可以被分解为对程序的遍历(pass over the program)。

Pass 在 LLVM 中主要分为两大类:

分析 Pass (Analysis Pass):只读取 IR,不修改它,产生可供其他 Pass 使用的分析结果。例如:

转换 Pass (Transformation Pass):修改 IR 以优化程序。例如:

从粒度上,Pass 还可以分为:

模块化设计的优势

Pass 的模块化设计带来了多重优势:

  1. 组合性:不同的 Pass 可以灵活组合,形成优化管线(pipeline)
  2. 可维护性:每个 Pass 的实现相对独立,易于理解和调试
  3. 可扩展性:添加新的优化只需实现新的 Pass
  4. 复用性:分析结果可以被多个转换 Pass 共享

这种设计理念可以用以下 ASCII 图表示:

   Input IR
      |
      v
 +-----------+
 | Pass A    |  <-- Analysis
 +-----------+
      |
      v
 +-----------+
 | Pass B    |  <-- Transformation (uses A's result)
 +-----------+
      |
      v
 +-----------+
 | Pass C    |  <-- Transformation
 +-----------+
      |
      v
   Output IR

Pass 执行模型

Pass Manager 负责协调 Pass 的执行,其核心职责包括:

  1. 调度:确定 Pass 的执行顺序
  2. 依赖管理:确保分析 Pass 在需要其结果的转换 Pass 之前运行
  3. 缓存管理:缓存分析结果,避免重复计算
  4. 失效处理:当 IR 被修改后,使相关分析结果失效

执行模型的核心是”懒惰求值”原则:分析结果只在需要时才计算,并尽可能长时间地保持有效。

3.2 从 Legacy PM 到 New PM:架构重构的必要性

Legacy Pass Manager 的历史与局限

Legacy Pass Manager 是 LLVM 项目早期(约 2003 年)由 Devang Patel 设计实现的。它基于继承的面向对象设计,所有 Pass 都继承自基类:

class Pass {
public:
  virtual bool runOnModule(Module &M) { return false; }
  virtual bool runOnFunction(Function &F) { return false; }
  // ...
};

这种设计在项目初期运作良好,但随着 LLVM 的发展,逐渐暴露出严重问题:

  1. 类型安全性差:Pass 之间的依赖通过 AnalysisID(本质是 void*)表达,缺乏类型检查
  2. 性能开销大:虚函数调用开销,特别是对于细粒度的 Pass
  3. 缓存策略僵化:分析结果的生命周期管理不够灵活
  4. 并行化困难:设计上没有考虑并行执行
  5. API 复杂:Pass 编写者需要理解复杂的继承层次

一个典型的 Legacy Pass 实现:

class MyFunctionPass : public FunctionPass {
  static char ID;
public:
  MyFunctionPass() : FunctionPass(ID) {}
  
  bool runOnFunction(Function &F) override {
    // 获取分析结果
    DominatorTree &DT = getAnalysis<DominatorTreeWrapperPass>().getDomTree();
    // 执行转换
    // ...
    return true; // IR 被修改
  }
  
  void getAnalysisUsage(AnalysisUsage &AU) const override {
    AU.addRequired<DominatorTreeWrapperPass>();
    AU.addPreserved<LoopInfoWrapperPass>();
  }
};

New Pass Manager 的设计动机

2014 年,Chandler Carruth 在 LLVM 开发者会议上提出了 New Pass Manager 的设计方案。主要动机包括:

  1. 类型安全:使用模板和概念(concepts)确保类型安全
  2. 性能优化:减少虚函数调用,改善缓存局部性
  3. 更灵活的缓存:细粒度的失效管理
  4. 简化 API:基于 lambda 和函数对象的设计
  5. 更好的可测试性:Pass 可以独立测试

New PM 的核心设计是基于模板的静态多态:

struct MyFunctionPass {
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) {
    // 获取分析结果
    auto &DT = FAM.getResult<DominatorTreeAnalysis>(F);
    // 执行转换
    // ...
    
    // 声明保留的分析
    PreservedAnalyses PA;
    PA.preserve<DominatorTreeAnalysis>();
    return PA;
  }
};

迁移过程中的挑战

从 Legacy PM 到 New PM 的迁移是一个长达数年的过程(2014-2021),主要挑战包括:

  1. 兼容性:需要同时支持两套系统
  2. 生态系统:大量第三方 Pass 需要迁移
  3. 性能回归:某些优化管线在 New PM 下表现不同
  4. 调试支持:需要重新实现调试和可视化工具

迁移策略采用了渐进式方法:

3.3 Pass 依赖性和调度算法

依赖关系的表达

Pass 之间存在复杂的依赖关系,主要包括:

  1. Required 依赖:转换 Pass 需要某些分析结果
  2. Preserved 关系:转换后哪些分析结果仍然有效
  3. Invalidated 关系:哪些分析需要重新计算

在 New PM 中,这些关系通过 PreservedAnalyses 类精确表达:

PreservedAnalyses PA;
PA.preserve<DominatorTreeAnalysis>();  // 保留支配树
PA.preserveSet<CFGAnalyses>();         // 保留所有 CFG 分析
PA.abandon<LoopAnalysis>();            // 显式废弃循环分析

依赖图可以表示为:

     ┌──────────────┐
     │ LoopAnalysis │
     └──────┬───────┘
            │ requires
            v
     ┌──────────────┐
     │ DomTreeAnalysis │
     └──────┬───────┘
            │ required by
            v
     ┌──────────────┐
     │   LICM Pass   │ ──preserves──> DomTreeAnalysis
     └──────────────┘

调度算法的演进

Pass Manager 的调度算法经历了多次演进:

Legacy PM 时代

New PM 的改进

调度算法的核心是解决优化顺序问题(Phase Ordering Problem)。不同的执行顺序可能导致不同的优化效果:

Scenario A: Inline → DeadCode → Simplify
Scenario B: DeadCode → Inline → Simplify

Scenario A 可能暴露更多死代码消除机会,而 Scenario B 可能减少内联的代码量。

Pass 顺序的优化

为了找到最优的 Pass 顺序,LLVM 采用了多种策略:

  1. 经验规则:基于大量实验得出的经典顺序
  2. 迭代执行:某些 Pass 组合会重复执行直到不动点
  3. Profile-Guided Ordering:基于程序特征调整顺序
  4. 机器学习方法:近期研究探索使用 ML 优化 Pass 顺序

一个典型的优化序列示例:

EarlyCSE → SimplifyCFG → SROA → EarlyCSE → 
InstCombine → SimplifyCFG → Reassociate → 
LoopRotate → LICM → LoopUnroll → InstCombine

3.4 分析信息的缓存与失效机制

分析结果的生命周期

分析结果的生命周期管理是 Pass Manager 的核心功能之一。一个分析结果从创建到销毁经历以下阶段:

  1. Lazy Construction:首次请求时构建
  2. Caching:存储在分析管理器中
  3. Reuse:被多个 Pass 复用
  4. Invalidation:当 IR 改变时失效
  5. Destruction:不再需要时销毁

生命周期可以用状态机表示:

    [Not Computed]
          |
          | request
          v
     [Computing]
          |
          | complete
          v
       [Valid] <──────┐
          |           │
          | invalidate│ recompute
          v           │
     [Invalid] ───────┘
          |
          | cleanup
          v
      [Destroyed]

缓存策略的设计

New PM 采用了分层缓存架构:

class FunctionAnalysisManager {
  // 每个函数的分析结果缓存
  DenseMap<pair<Function*, AnalysisKey*>, unique_ptr<AnalysisResult>> Cache;
  
  template<typename AnalysisT>
  typename AnalysisT::Result& getResult(Function &F) {
    auto Key = make_pair(&F, AnalysisT::ID());
    auto It = Cache.find(Key);
    if (It != Cache.end())
      return *static_cast<AnalysisT::Result*>(It->second.get());
    
    // 运行分析并缓存结果
    auto Result = AnalysisT().run(F, *this);
    Cache[Key] = make_unique<...>(move(Result));
    return *Cache[Key];
  }
};

缓存策略的关键设计决策:

  1. 粒度控制:Module、Function、Loop 级别的独立缓存
  2. 内存管理:使用智能指针自动管理生命周期
  3. 并发访问:未来版本考虑线程安全
  4. 缓存大小:无限制缓存 vs. LRU 淘汰

失效传播机制

当 IR 被修改后,相关的分析结果需要失效。New PM 使用 PreservedAnalyses 实现细粒度的失效控制:

PreservedAnalyses InstCombinePass::run(Function &F, FunctionAnalysisManager &FAM) {
  // 执行指令组合优化
  bool Changed = combineInstructions(F);
  
  if (!Changed)
    return PreservedAnalyses::all();  // 没有修改,保留所有分析
  
  PreservedAnalyses PA;
  PA.preserveSet<CFGAnalyses>();      // 保留 CFG 相关分析
  PA.abandon<ScalarEvolutionAnalysis>(); // SCEV 需要重算
  return PA;
}

失效传播的规则:

  1. 显式保留:Pass 声明保留的分析继续有效
  2. 传递失效:依赖失效分析的其他分析也失效
  3. 保守策略:不确定时假设失效
  4. 集合操作:支持分析集合的批量保留/失效

失效传播示例:

Pass 修改了 CFG
  → BasicBlock 结构改变
    → DominatorTree 失效
      → LoopInfo 失效(依赖 DomTree)
        → ScalarEvolution 失效(依赖 LoopInfo)

3.5 优化 Pass 的组合策略:-O0 到 -O3 的演进

优化级别的设计哲学

LLVM 的优化级别继承了 GCC 的传统,但在实现上有自己的理解:

-O0(无优化)

-O1(基础优化)

-O2(平衡优化)

-O3(激进优化)

-Os(大小优化)

-Oz(极限大小优化)

Pipeline 的构建原则

优化 Pipeline 的构建遵循以下原则:

  1. 规范化优先:先将 IR 转换为规范形式
  2. 由简到繁:先运行快速简单的优化
  3. 迭代改进:某些 Pass 组合重复执行
  4. 相互促进:Pass 之间的协同效应

典型的 -O2 Pipeline 结构:

Module Pipeline:
├── Force Function Attrs
├── Infer Function Attrs
├── [CGSCC Pipeline]
│   ├── Function Simplification Pipeline (iteration 1)
│   │   ├── SROA
│   │   ├── Early CSE
│   │   ├── SimplifyCFG
│   │   ├── InstCombine
│   │   └── ...
│   ├── Inline
│   └── Function Simplification Pipeline (iteration 2)
└── Module Optimization Pipeline
    ├── Global DCE
    ├── Global Optimizer
    └── ...

性能与编译时间的平衡

优化级别的选择本质上是性能提升与编译时间的权衡:

性能提升 (%)
^
│     ╭────── -O3
│    ╱
│   ╱╱───── -O2
│  ╱╱
│ ╱╱──── -O1
│╱
└────────────────> 编译时间 (x)

关键的权衡点:

  1. 内联阈值
    • -O1: threshold = 75
    • -O2: threshold = 225
    • -O3: threshold = 275
  2. 循环展开因子
    • -O1: 不展开
    • -O2: 展开因子 2-4
    • -O3: 展开因子 4-8
  3. 向量化策略
    • -O1: 禁用
    • -O2: 启用基本向量化
    • -O3: 激进向量化,包括 gather/scatter

实际案例:Sanjoy Das 在 2018 年的 Pipeline 调优将 Chrome 的某些关键路径性能提升了 5%,同时编译时间仅增加 2%。

3.6 高级话题:Coroutine Passes 的设计挑战与 CO-RE 技术

Coroutine 转换的复杂性

C++20 引入的 Coroutine 给 LLVM 的 Pass 系统带来了独特挑战。Coroutine 的实现需要将看似连续的函数体转换为可以暂停和恢复的状态机,这涉及复杂的 IR 转换。

Coroutine 转换的核心步骤:

  1. Coroutine Splitting:将函数分割为多个部分
    • Initial suspend point
    • Resume points
    • Final suspend point
    • Destroy function
  2. Frame Allocation:创建 coroutine frame 保存局部状态

  3. Suspend Point Lowering:将 suspend 点转换为状态机跳转

转换示例:

// 原始 Coroutine
generator<int> range(int n) {
  for (int i = 0; i < n; ++i)
    co_yield i;
}

// 转换后的伪代码结构
struct range_frame {
  int n, i;
  int suspend_index;
  promise_type promise;
};

void range_resume(range_frame* frame) {
  switch(frame->suspend_index) {
    case 0: goto resume_point_0;
    case 1: goto resume_point_1;
  }
  
resume_point_0:
  // for loop body
  frame->promise.yield_value(frame->i);
  frame->suspend_index = 1;
  return;
  
resume_point_1:
  // continue loop
  // ...
}

Coroutine Pass Pipeline

LLVM 的 Coroutine 转换通过一系列专门的 Pass 实现:

CoroEarly
  ↓
[Regular Optimization Passes]
  ↓
CoroSplit
  ├── CoroElide (优化:消除堆分配)
  └── CoroFrame (构建 coroutine frame)
      ↓
CoroCleanup

关键的设计挑战:

  1. 与优化 Pass 的交互
    • Coroutine intrinsics 需要在特定时机展开
    • 某些优化可能破坏 coroutine 语义
    • 需要特殊的别名分析支持
  2. 跨 suspend 点的优化
    • 传统的数据流分析需要适配
    • 活跃变量分析变得复杂
    • 寄存器分配策略需要调整
  3. 异常处理
    • Coroutine 的异常传播机制
    • Frame 销毁的正确性保证

CO-RE (Compile Once - Run Everywhere) 技术

CO-RE 是 BPF(Berkeley Packet Filter)生态系统中的一项关键技术,它允许编译一次的 BPF 程序在不同内核版本上运行。虽然不直接属于 Coroutine,但 CO-RE 展示了 LLVM 如何支持特殊的编译需求。

CO-RE 的核心机制:

  1. BTF (BPF Type Format)
    • 保留类型信息到目标代码
    • 支持运行时类型匹配
  2. Field Relocation
    • 编译时记录字段访问
    • 加载时根据实际内核调整偏移
// CO-RE 示例
struct task_struct {
  // 不同内核版本字段偏移可能不同
  int pid;
  // ...
};

// 使用 CO-RE
int get_pid(struct task_struct *task) {
  return BPF_CORE_READ(task, pid);  // 运行时解析正确偏移
}
  1. LLVM 的支持
    • 特殊的 relocation 记录
    • BPF 后端的 CO-RE 支持
    • Clang 的 __builtin_preserve_* intrinsics

CO-RE 在 Pass 系统中的处理:

Source Code
    ↓
Clang (生成 CO-RE relocations)
    ↓
LLVM IR (带 preserve intrinsics)
    ↓
BPF Backend (保留 relocation 信息)
    ↓
BPF Object (带 BTF 和 relocations)
    ↓
Kernel Loader (运行时 relocation)

未来发展方向

Coroutine 和 CO-RE 技术的发展趋势:

  1. Coroutine 优化增强
    • 更好的 frame 大小优化
    • 跨 suspend 点的激进优化
    • 对称 coroutine 的支持
  2. 异构计算支持
    • GPU coroutine 的探索
    • 分布式 coroutine 框架
  3. CO-RE 扩展
    • 用户态程序的 CO-RE 支持
    • 跨平台二进制兼容性

3.7 核心人物与关键事件

Devang Patel 与 Legacy Pass Manager (2003-2010)

Devang Patel 在 Apple 工作期间主导了 Legacy Pass Manager 的设计和实现。他的主要贡献包括:

关键设计决策:

Chandler Carruth 的 New Pass Manager 革命 (2014-2018)

Chandler Carruth 在 Google 工作期间推动了 New Pass Manager 的开发。他在 2014 年 LLVM 开发者会议上的演讲 “The New Pass Manager” 成为转折点。

主要改进:

他的名言:”The old pass manager is where performance goes to die”(旧的 Pass 管理器是性能的坟墓)深刻影响了社区。

Sanjoy Das 与 Pass Pipeline 调优 (2016-2019)

Sanjoy Das 在 Azul Systems 和后来的 Google 工作期间,对 Pass Pipeline 进行了系统性优化:

  1. 数据驱动的优化顺序
    • 收集大规模程序的优化效果数据
    • 使用 A/B 测试验证 Pipeline 改动
  2. 关键贡献
    • Loop Pass Manager 的重构
    • Phase Ordering 的实证研究
    • PGO 集成的改进
  3. 影响力项目
    • Chrome V8 引擎的编译优化
    • TensorFlow XLA 的 LLVM 集成

历史时间线

2003: Legacy Pass Manager 初版发布
2005: Pass Manager 支持 Loop Pass
2008: 引入 CallGraphSCCPass
2010: 开始讨论 Pass Manager 重构
2014: Chandler Carruth 提出 New PM 设计
2015: New PM 基础设施开发开始
2016: 第一批 Pass 迁移到 New PM
2017: Clang 开始实验性支持 New PM
2018: New PM 在 -O3 下默认启用
2019: 大规模 A/B 测试验证 New PM
2020: New PM 成为默认选项
2021: Legacy PM 标记为废弃
2022: 开始移除 Legacy PM 代码

本章小结

Pass 基础设施是 LLVM 成功的关键因素之一。从 Legacy Pass Manager 到 New Pass Manager 的演进,体现了软件工程中的重要原则:模块化、类型安全、性能优化和渐进式重构。

核心要点回顾:

  1. 模块化设计:Pass 机制将复杂的编译优化分解为可组合的单元,提高了代码的可维护性和可扩展性。

  2. 依赖管理:精确的依赖表达和缓存机制确保了分析结果的正确性和效率。

  3. 架构演进:从 Legacy PM 到 New PM 的迁移展示了大型系统重构的最佳实践。

  4. 优化策略:不同优化级别(-O0 到 -O3)的设计平衡了编译时间和运行时性能。

  5. 新技术挑战:Coroutine 和 CO-RE 等新技术要求 Pass 系统不断演进。

关键公式和概念:

练习题

基础题

练习 3.1:解释 Analysis Pass 和 Transformation Pass 的区别,并各举两个例子。

答案 Analysis Pass 只读取 IR 不修改它,产生供其他 Pass 使用的信息。例如:DominatorTree(构建支配树)、AliasAnalysis(别名分析)。 Transformation Pass 修改 IR 以优化程序。例如:InstCombine(指令组合)、LICM(循环不变代码外提)。 关键区别在于 Analysis Pass 不改变程序语义和结构,而 Transformation Pass 会修改 IR 但保持语义等价。

练习 3.2:为什么 New Pass Manager 使用模板而不是虚函数?列出至少三个优势。

答案 1. **类型安全**:编译时类型检查,避免运行时转换错误 2. **性能优化**:消除虚函数调用开销,支持内联优化 3. **更好的错误信息**:模板错误在编译时发现,更容易调试 4. **灵活的接口**:不需要固定的基类接口,Pass 可以有不同的签名

练习 3.3:描述 -O2 和 -O3 优化级别的主要区别。

答案 -O2 追求性能和编译时间的平衡,包含完整的标量和循环优化、积极但受控的内联、基本向量化。 -O3 追求最大性能,特点包括:更激进的循环展开(因子 4-8 vs 2-4)、更高的内联阈值(275 vs 225)、激进的向量化包括 gather/scatter、可能增加代码体积。 主要权衡:-O3 可能带来 10-20% 额外性能提升,但编译时间增加 2-4 倍,代码体积可能增加 20-50%。

挑战题

练习 3.4:设计一个简单的 Pass 依赖图,包含至少 4 个 Pass,其中 2 个是 Analysis Pass,2 个是 Transformation Pass。标注它们的 required 和 preserved 关系。

提示 考虑一个典型的优化序列:首先需要 CFG 分析,然后是支配树,基于这些信息进行循环优化,最后进行死代码消除。
答案 ``` CFGAnalysis ↓ (required by) DominatorTreeAnalysis ↓ (required by) LoopSimplifyPass ──preserves→ CFGAnalysis, DominatorTreeAnalysis ↓ (invalidates) DeadCodeEliminationPass ──preserves→ Nothing ``` 依赖关系: - LoopSimplifyPass requires: CFGAnalysis, DominatorTreeAnalysis - LoopSimplifyPass preserves: CFGAnalysis, DominatorTreeAnalysis - DeadCodeEliminationPass requires: CFGAnalysis - DeadCodeEliminationPass preserves: None (可能改变 CFG)

练习 3.5:假设你要从 Legacy PM 迁移一个自定义 Pass 到 New PM,列出需要修改的主要部分。

提示 考虑继承关系、API 差异、分析获取方式、保留声明方式的变化。
答案 主要修改: 1. **移除继承**:从 `class MyPass : public FunctionPass` 改为 `struct MyPass` 2. **修改 run 方法**:从 `runOnFunction(Function &F)` 改为 `run(Function &F, FunctionAnalysisManager &FAM)` 3. **分析获取**:从 `getAnalysis()` 改为 `FAM.getResult(F)` 4. **保留声明**:从 `getAnalysisUsage()` 改为返回 `PreservedAnalyses` 对象 5. **Pass 注册**:使用新的注册宏和管理器 API 6. **移除 ID**:不再需要 `static char ID` </details> **练习 3.6**:解释 Coroutine 转换为什么需要特殊的 Pass Pipeline,以及主要的技术挑战。
提示 考虑 Coroutine 的暂停/恢复语义、状态保存需求、与普通优化的交互。
答案 Coroutine 需要特殊 Pipeline 的原因: 1. **语义转换复杂性**:需要将线性代码转换为状态机,涉及函数分割、frame 分配、suspend 点处理 2. **优化时机敏感**: - CoroEarly 必须在优化前运行,标记 coroutine - CoroSplit 需要在某些优化后但在后端前运行 - 过早优化可能破坏 coroutine 结构 3. **技术挑战**: - 跨 suspend 点的数据流分析 - Frame 大小优化(决定哪些变量需要保存) - 与异常处理的正确交互 - 堆分配的优化(CoroElide) 4. **特殊的分析需求**:传统分析假设函数连续执行,需要适配暂停/恢复语义
**练习 3.7**:假设你要设计一个新的优化级别 -O2.5,介于 -O2 和 -O3 之间,你会如何设置关键参数?
提示 考虑内联阈值、循环展开因子、向量化策略等关键参数的渐进式调整。
答案 -O2.5 设计方案: 1. **内联阈值**:250(-O2: 225, -O3: 275) 2. **循环展开**:因子 3-4(-O2: 2-4, -O3: 4-8) 3. **向量化**:启用中等激进的向量化,但避免 gather/scatter 4. **其他优化**: - 启用部分 -O3 的标量优化 - 保守的推测执行优化 - 限制代码膨胀的优化 目标:获得 -O3 约 70% 的性能提升,编译时间增加控制在 50% 以内,代码体积增加不超过 15%。 这种设计适合对性能有较高要求但又关注编译时间和代码大小的场景。
**练习 3.8**:设计一个实验来测量不同 Pass 顺序对优化效果的影响。
提示 考虑基准测试选择、度量指标、控制变量、统计方法。
答案 实验设计: 1. **基准测试集**: - SPEC CPU2017 的子集 - 实际应用(SQLite、Redis、nginx) - 微基准测试(循环密集、分支密集、内存密集) 2. **Pass 顺序变体**: - Baseline: 标准 -O2 顺序 - Variant A: InstCombine → LICM → Vectorize - Variant B: LICM → Vectorize → InstCombine - Variant C: Vectorize → LICM → InstCombine 3. **度量指标**: - 执行时间(多次运行取中位数) - 编译时间 - 代码大小 - 缓存未命中率 4. **实验步骤**: - 对每个基准测试和每个变体编译 - 运行 10 次,去除异常值 - 计算相对于 Baseline 的改进 - 统计分析(t-test)确定显著性 5. **预期结果**:不同类型程序对 Pass 顺序敏感度不同,循环密集型程序对 LICM 位置更敏感。
## 常见陷阱与错误 (Gotchas) ### 1. Pass 依赖循环 **问题**:创建循环依赖导致死锁或栈溢出。 ```cpp // 错误示例 PassA requires PassB PassB requires PassA // 循环依赖! ``` **解决方案**:仔细设计 Pass 层次,使用分层架构避免循环。 ### 2. 过度保守的 Preserved 声明 **问题**:Pass 声明保留了实际已经失效的分析。 ```cpp // 错误:修改了 CFG 但声明保留 PA.preserveSet(); // 实际上 CFG 已改变 ``` **后果**:使用过期分析结果导致错误优化或崩溃。 **解决方案**:宁可保守(不声明保留)也不要错误声明。 ### 3. 分析结果的生命周期误用 **问题**:在 Pass 执行后继续使用分析结果指针。 ```cpp DominatorTree *DT = &AM.getResult(F); // ... 运行其他可能使 DT 失效的 Pass ... DT->dominates(...); // 危险!DT 可能已失效 ``` **解决方案**:在需要时重新获取分析结果,不要长期持有指针。 ### 4. New PM 迁移的 API 混淆 **问题**:混用 Legacy 和 New PM 的 API。 **解决方案**:完全迁移到 New PM,使用一致的 API。 ### 5. 优化级别的误解 **问题**:认为 -O3 总是比 -O2 好。 **现实**:-O3 可能导致代码膨胀、缓存压力增加,某些情况下性能反而下降。 **解决方案**:基于实际测试选择优化级别,考虑 PGO。 ## 最佳实践检查清单 ### Pass 设计检查清单 - [ ] Pass 功能单一、职责明确 - [ ] 正确声明所需的分析 (Required) - [ ] 准确声明保留的分析 (Preserved) - [ ] 避免不必要的 IR 遍历 - [ ] 使用高效的数据结构(DenseMap vs std::map) - [ ] 提供有意义的统计信息 - [ ] 包含适当的 DEBUG 输出 - [ ] 编写单元测试和 lit 测试 ### 性能优化检查清单 - [ ] 测量编译时间影响 - [ ] 评估内存使用 - [ ] 验证优化效果(使用基准测试) - [ ] 考虑 Pass 顺序的影响 - [ ] 检查缓存命中率 - [ ] 评估对不同类型程序的影响 ### New PM 迁移检查清单 - [ ] 移除所有 Legacy PM 依赖 - [ ] 使用模板而非继承 - [ ] 更新分析获取方式 - [ ] 修改保留声明方式 - [ ] 更新 Pass 注册 - [ ] 验证功能等价性 - [ ] 性能回归测试 ### 调试技巧清单 - [ ] 使用 `-debug-pass-manager` 查看 Pass 执行顺序 - [ ] 使用 `-print-after-all` 查看每个 Pass 后的 IR - [ ] 使用 `-time-passes` 分析 Pass 耗时 - [ ] 使用 `-stats` 查看 Pass 统计信息 - [ ] 编写专门的测试用例隔离问题 - [ ] 使用 `opt` 工具单独测试 Pass