使用新的 Pass Manager

概述

有关新 Pass Manager 的概述,请参阅 博客文章

告诉我如何使用新的 Pass Manager 运行默认优化流水线

// Create the analysis managers.
// These must be declared in this order so that they are destroyed in the
// correct order due to inter-analysis-manager references.
LoopAnalysisManager LAM;
FunctionAnalysisManager FAM;
CGSCCAnalysisManager CGAM;
ModuleAnalysisManager MAM;

// Create the new pass manager builder.
// Take a look at the PassBuilder constructor parameters for more
// customization, e.g. specifying a TargetMachine or various debugging
// options.
PassBuilder PB;

// Register all the basic analyses with the managers.
PB.registerModuleAnalyses(MAM);
PB.registerCGSCCAnalyses(CGAM);
PB.registerFunctionAnalyses(FAM);
PB.registerLoopAnalyses(LAM);
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);

// Create the pass manager.
// This one corresponds to a typical -O2 optimization pipeline.
ModulePassManager MPM = PB.buildPerModuleDefaultPipeline(OptimizationLevel::O2);

// Optimize the IR!
MPM.run(MyModule, MAM);

C API 也支持大部分功能,请参阅 llvm-c/Transforms/PassBuilder.h

向 Pass Manager 添加 Pass

有关如何编写新的 PM Pass,请参阅 此页面

要向新的 PM Pass Manager 添加 Pass,重要的是要匹配 Pass 类型和 Pass Manager 类型。例如,FunctionPassManager 只能包含函数 Pass。

FunctionPassManager FPM;
// InstSimplifyPass is a function pass
FPM.addPass(InstSimplifyPass());

如果要将一个在函数中所有循环上运行的循环 Pass 添加到 FunctionPassManager,则必须将该循环 Pass 包裹在一个函数 Pass 适配器中,该适配器遍历函数中的所有循环并在每个循环上运行循环 Pass。

FunctionPassManager FPM;
// LoopRotatePass is a loop pass
FPM.addPass(createFunctionToLoopPassAdaptor(LoopRotatePass()));

从新 PM 的角度来看,IR 的层次结构是 Module -> (CGSCC ->) Function -> Loop,其中遍历 CGSCC 是可选的。

FunctionPassManager FPM;
// loop -> function
FPM.addPass(createFunctionToLoopPassAdaptor(LoopFooPass()));

CGSCCPassManager CGPM;
// loop -> function -> cgscc
CGPM.addPass(createCGSCCToFunctionPassAdaptor(createFunctionToLoopPassAdaptor(LoopFooPass())));
// function -> cgscc
CGPM.addPass(createCGSCCToFunctionPassAdaptor(FunctionFooPass()));

ModulePassManager MPM;
// loop -> function -> module
MPM.addPass(createModuleToFunctionPassAdaptor(createFunctionToLoopPassAdaptor(LoopFooPass())));
// function -> module
MPM.addPass(createModuleToFunctionPassAdaptor(FunctionFooPass()));

// loop -> function -> cgscc -> module
MPM.addPass(createModuleToPostOrderCGSCCPassAdaptor(createCGSCCToFunctionPassAdaptor(createFunctionToLoopPassAdaptor(LoopFooPass()))));
// function -> cgscc -> module
MPM.addPass(createModuleToPostOrderCGSCCPassAdaptor(createCGSCCToFunctionPassAdaptor(FunctionFooPass())));

特定 IR 单位的 Pass Manager 也是该类型的 Pass。例如,FunctionPassManager 是一个函数 Pass,这意味着它可以添加到 ModulePassManager 中。

ModulePassManager MPM;

FunctionPassManager FPM;
// InstSimplifyPass is a function pass
FPM.addPass(InstSimplifyPass());

MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));

通常,您希望将 CGSCC/函数/循环 Pass 组合到一个 Pass Manager 中,而不是为每个 Pass 添加适配器到包含的上层 Pass Manager 中。例如,

ModulePassManager MPM;
MPM.addPass(createModuleToFunctionPassAdaptor(FunctionPass1()));
MPM.addPass(createModuleToFunctionPassAdaptor(FunctionPass2()));
MPM.run();

将在模块中的每个函数上运行 FunctionPass1,然后在模块中的每个函数上运行 FunctionPass2。相反,

ModulePassManager MPM;

FunctionPassManager FPM;
FPM.addPass(FunctionPass1());
FPM.addPass(FunctionPass2());

MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));

将在模块中的第一个函数上运行 FunctionPass1FunctionPass2,然后在模块中的第二个函数上运行这两个 Pass,依此类推。这对于 LLVM 数据结构周围的缓存局部性更好。这同样适用于其他 IR 类型,并且在某些情况下甚至会影响优化的质量。例如,在循环上运行所有循环 Pass 可能会导致以后的循环能够比每个循环 Pass 单独运行时获得更好的优化。

将 Pass 插入默认流水线

而不是手动将 Pass 添加到 Pass Manager 中,创建 Pass Manager 的典型方法是使用 PassBuilder 并调用类似 PassBuilder::buildPerModuleDefaultPipeline() 的内容,它会为给定的优化级别创建一个典型的流水线。

有时前端或后端会希望将 Pass 注入到流水线中。例如,前端可能希望添加检测,而目标后端可能希望添加降低自定义内在函数的 Pass。对于这些情况,PassBuilder 公开了回调,允许将 Pass 注入到流水线的某些部分。例如,

PassBuilder PB;
PB.registerPipelineStartEPCallback(
    [&](ModulePassManager &MPM, PassBuilder::OptimizationLevel Level) {
      MPM.addPass(FooPass());
    });

将在由该 PassBuilder 创建的 Pass Manager 的流水线的最开始附近添加 FooPass。有关可以在哪些位置添加 Pass,请参阅 PassBuilder 的文档。

如果 PassBuilder 具有与后端对应的 TargetMachine,它将调用 TargetMachine::registerPassBuilderCallbacks() 以允许后端将 Pass 注入到流水线中。

Clang 的 BackendUtil.cpp 显示了前端将(主要是 sanitizer)Pass 添加到流水线各个部分的示例。AMDGPUTargetMachine::registerPassBuilderCallbacks() 是后端将 Pass 添加到流水线各个部分的示例。

Pass 插件也可以将 Pass 添加到默认流水线中。不同的工具有不同的加载动态 Pass 插件的方法。例如,opt -load-pass-plugin=path/to/plugin.so 将 Pass 插件加载到 opt 中。有关编写 Pass 插件的信息,请参阅 编写 LLVM Pass

使用分析

LLVM 提供了许多 Pass 可以使用的分析,例如支配树。计算这些分析可能很昂贵,因此新的 Pass Manager 具有缓存分析并在可能的情况下重用它们的机制。

当 Pass 在某些 IR 上运行时,它还会收到一个分析管理器,它可以使用该管理器查询分析。查询分析将导致管理器检查它是否已经为请求的 IR 计算了结果。如果它已经计算并且结果仍然有效,它将返回该结果。否则,它将通过调用分析的 run() 方法构造一个新的结果,将其缓存并返回。您还可以要求分析管理器仅在结果已缓存时才返回分析。

分析管理器仅为与 Pass 运行的 IR 类型相同的 IR 类型提供分析结果。例如,函数 Pass 会收到一个仅提供函数级分析的分析管理器。这适用于许多在固定范围内工作的 Pass。但是,某些 Pass 希望查看 IR 层次结构的上方或下方。例如,SCC Pass 可能希望查看 SCC 内函数的函数分析。或者它可能希望查看一些不可变的全局分析。在这些情况下,分析管理器可以提供到外部或内部级别分析管理器的代理。例如,要从 CGSCCAnalysisManager 获取 FunctionAnalysisManager,您可以调用

FunctionAnalysisManager &FAM =
    AM.getResult<FunctionAnalysisManagerCGSCCProxy>(InitialC, CG)
        .getManager();

并将 FAM 用作函数 Pass 可以访问的典型 FunctionAnalysisManager。要访问外部级别 IR 分析,您可以调用

const auto &MAMProxy =
    AM.getResult<ModuleAnalysisManagerCGSCCProxy>(InitialC, CG);
FooAnalysisResult *AR = MAMProxy.getCachedResult<FooAnalysis>(M);

请求缓存的和不可变的外部级别 IR 分析可以通过 getCachedResult() 工作,但不允许直接访问外部级别 IR 分析管理器来计算外部级别 IR 分析。这是由于几个原因。

第一个原因是在内部级别 IR Pass 中跨外部级别 IR 运行分析会导致二次编译时间行为。例如,模块分析通常扫描每个函数,并且允许函数 Pass 运行模块分析可能会导致我们二次扫描函数。如果 Pass 可以保持外部级别分析的最新状态而不是按需计算它们,这将不是问题,但这将需要大量工作来确保每个 Pass 更新所有外部级别分析,到目前为止,这还没有必要,也没有相应的机制(除了下面描述的循环 Pass 中的函数分析)。优雅降级的自我更新分析也处理此问题(例如 GlobalsAA),但它们遇到了如果我们想要精度则必须在优化流水线的某个地方手动重新计算的问题,并且它们阻止了潜在的未来并发性。

第二个原因是要记住潜在的未来 Pass 并发性,例如在 CGSCC 或模块中不同函数上并行化函数 Pass。由于 Pass 可以请求缓存的分析结果,因此如果支持并发性,允许 Pass 触发外部级别分析计算可能会导致不确定性。一个相关的限制是,内部 Pass 使用的外部级别 IR 分析必须是不可变的,否则它们可能会因对内部级别 IR 的更改而失效。内部 Pass 未使用的外部分析可能会因对内部级别 IR 的更改而失效。这些失效发生在内部 Pass Manager 完成之后,因此访问可变分析将给出无效的结果。

无法访问外部级别分析的例外情况是在循环 Pass 中访问函数分析。循环 Pass 通常使用函数分析,例如支配树。循环 Pass 本质上需要修改循环所在的函数,包括循环分析依赖的一些函数分析。这会忽略未来在函数中单独循环上的并发性,但这是由于循环及其函数之间的紧密耦合造成的权衡。为了确保循环 Pass 使用的函数分析有效,它们会在循环 Pass 中手动更新,以确保不需要失效。有一组循环 Pass 和分析可以访问的常用函数分析,它们作为 LoopStandardAnalysisResults 参数传递给循环 Pass。其他可变函数分析无法从循环 Pass 访问。

与任何缓存机制一样,我们需要一些方法来告诉分析管理器何时结果不再有效。分析管理器的大部分复杂性来自尝试使尽可能少的分析结果失效,以使编译时间尽可能短。

有两种方法可以处理可能无效的分析结果。一种是简单地强制清除结果。这通常应该只在结果关联的 IR 变为无效时使用。例如,函数被删除,或者由于调用图更改导致 CGSCC 变为无效。

使分析结果失效的典型方法是,让一个 Pass 声明它保留哪些类型的分析,以及它不保留哪些类型的分析。在转换 IR 时,Pass 既可以选择在 IR 转换的同时更新分析,也可以告诉分析管理器分析不再有效,应该失效。如果一个 Pass 想要保持某些特定分析的更新,例如当更新比失效并重新计算更快时,分析本身可能会有方法来针对特定的转换进行更新,或者可能会有像 DomTreeUpdater 这样的辅助更新器来更新 DominatorTree。否则,要将某些分析标记为不再有效,Pass 可以返回一个带有相应分析失效的 PreservedAnalyses

// We've made no transformations that can affect any analyses.
return PreservedAnalyses::all();

// We've made transformations and don't want to bother to update any analyses.
return PreservedAnalyses::none();

// We've specifically updated the dominator tree alongside any transformations, but other analysis results may be invalid.
PreservedAnalyses PA;
PA.preserve<DominatorAnalysis>();
return PA;

// We haven't made any control flow changes, any analyses that only care about the control flow are still valid.
PreservedAnalyses PA;
PA.preserveSet<CFGAnalyses>();
return PA;

Pass 管理器将使用 Pass 返回的 PreservedAnalyses 调用分析管理器的 invalidate() 方法。这也可以在 Pass 内部手动完成。

FooModulePass::run(Module& M, ModuleAnalysisManager& AM) {
  auto &FAM = AM.getResult<FunctionAnalysisManagerModuleProxy>(M).getManager();

  // Invalidate all analysis results for function F1.
  FAM.invalidate(F1, PreservedAnalyses::none());

  // Invalidate all analysis results across the entire module.
  AM.invalidate(M, PreservedAnalyses::none());

  // Clear the entry in the analysis manager for function F2 if we've completely removed it from the module.
  FAM.clear(F2);

  ...
}

在访问内部级别 IR 分析时需要注意的一点是,已删除 IR 的缓存结果。如果在模块 Pass 中删除了一个函数,它的地址仍然用作缓存分析的键。在 Pass 中要注意,要么清除该函数的结果,要么根本不使用内部分析。

AM.invalidate(M, PreservedAnalyses::none()); 将使内部分析管理器代理失效,这将清除所有缓存的分析,保守地假设有无效的地址用作缓存分析的键。但是,如果您想更选择性地缓存/使哪些分析失效,您可以将分析管理器代理标记为保留,基本上表示所有已删除的条目都已手动处理。这应该只在有可衡量的编译时间收益的情况下进行,因为确保所有正确的分析都失效可能很棘手。

实现分析失效

默认情况下,如果 PreservedAnalyses 表示它运行的 IR 单元上的分析没有被保留(参见 AnalysisResultModel::invalidate()),则分析将失效。分析可以实现 invalidate(),以便在失效方面更加保守。例如,

bool FooAnalysisResult::invalidate(Function &F, const PreservedAnalyses &PA,
                                   FunctionAnalysisManager::Invalidator &) {
  auto PAC = PA.getChecker<FooAnalysis>();
  // the default would be:
  // return !(PAC.preserved() || PAC.preservedSet<AllAnalysesOn<Function>>());
  return !(PAC.preserved() || PAC.preservedSet<AllAnalysesOn<Function>>()
      || PAC.preservedSet<CFGAnalyses>());
}

表示如果 PreservedAnalyses 特别保留了 FooAnalysis,或者如果 PreservedAnalyses 保留了所有分析(在 PAC.preserved() 中隐含),或者如果 PreservedAnalyses 保留了所有函数分析,或者 PreservedAnalyses 保留了所有仅关心 CFG 的分析,则 FooAnalysisResult 不应失效。

如果分析是无状态的,并且通常不应该失效,请使用以下方法

bool FooAnalysisResult::invalidate(Function &F, const PreservedAnalyses &PA,
                                   FunctionAnalysisManager::Invalidator &) {
  // Check whether the analysis has been explicitly invalidated. Otherwise, it's
  // stateless and remains preserved.
  auto PAC = PA.getChecker<FooAnalysis>();
  return !PAC.preservedWhenStateless();
}

如果分析依赖于其他分析,则还需要检查这些分析是否失效。

bool FooAnalysisResult::invalidate(Function &F, const PreservedAnalyses &PA,
                                   FunctionAnalysisManager::Invalidator &Inv) {
  auto PAC = PA.getChecker<FooAnalysis>();
  if (!PAC.preserved() && !PAC.preservedSet<AllAnalysesOn<Function>>())
    return true;

  // Check transitive dependencies.
  return Inv.invalidate<BarAnalysis>(F, PA) ||
        Inv.invalidate<BazAnalysis>(F, PA);
}

失效和分析管理器代理的组合导致了一些复杂性。例如,当我们在模块 Pass 中使所有分析失效时,我们必须确保我们也使通过任何现有内部代理可访问的函数分析失效。内部代理的 invalidate() 首先检查代理本身是否应该失效。如果是,则意味着代理可能包含指向不再有效的 IR 的指针,这意味着内部代理需要完全清除所有相关的分析结果。否则,代理只是将失效转发到内部分析管理器。

通常对于外部代理,来自外部分析管理器的分析结果应该是不可变的,因此失效不应该是一个问题。但是,某些内部分析可能依赖于某些外部分析,当外部分析失效时,我们需要确保相关的内部分析也失效。这实际上发生在别名分析结果中。别名分析是函数级别的分析,但存在特定类型别名分析的模块级别实现。目前 GlobalsAA 是唯一的模块级别别名分析,并且通常不会失效,因此这不是什么大问题。有关更多详细信息,请参见 OuterAnalysisManagerProxy::Result::registerOuterAnalysisInvalidation()

调用 opt

$ opt -passes='pass1,pass2' /tmp/a.ll -S
# -p is an alias for -passes
$ opt -p pass1,pass2 /tmp/a.ll -S

新的 PM 通常需要显式 Pass 嵌套。例如,要运行函数 Pass,然后是模块 Pass,我们需要将函数 Pass 包裹在一个模块适配器中。

$ opt -passes='function(no-op-function),no-op-module' /tmp/a.ll -S

一个更完整的例子,以及 -debug-pass-manager 来显示执行顺序。

$ opt -passes='no-op-module,cgscc(no-op-cgscc,function(no-op-function,loop(no-op-loop))),function(no-op-function,loop(no-op-loop))' /tmp/a.ll -S -debug-pass-manager

不正确的嵌套会导致错误消息,例如

$ opt -passes='no-op-function,no-op-module' /tmp/a.ll -S
opt: unknown function pass 'no-op-module'

嵌套关系是:模块 (-> cgscc) -> 函数 -> 循环,其中 CGSCC 嵌套是可选的。

有几个特殊情况可以简化输入。

  • 如果第一个 Pass 不是模块 Pass,则会隐式创建第一个 Pass 的 Pass 管理器。

    • 例如,以下代码等效:

$ opt -passes='no-op-function,no-op-function' /tmp/a.ll -S
$ opt -passes='function(no-op-function,no-op-function)' /tmp/a.ll -S
  • 如果存在一个适配器,可以使 Pass 适应之前的 Pass 管理器,则会隐式创建该适配器。

    • 例如,以下代码等效:

$ opt -passes='no-op-function,no-op-loop' /tmp/a.ll -S
$ opt -passes='no-op-function,loop(no-op-loop)' /tmp/a.ll -S

要获取可用 Pass 和分析的列表,包括它们操作的 IR 单元(模块、CGSCC、函数、循环),请运行

$ opt --print-passes

或查看 PassRegistry.def

要确保在 Pass 之前可以使用名为 foo 的分析,请将 require<foo> 添加到 Pass 管道中。这会添加一个 Pass,该 Pass 只是请求运行分析。此 Pass 也受正确的嵌套约束。例如,要确保在模块 Pass 之前已为所有函数计算了一些函数分析

$ opt -passes='function(require<my-function-analysis>),my-module-pass' /tmp/a.ll -S

新旧 Pass 管理器的状态

LLVM 目前包含两个 Pass 管理器,旧的 PM 和新的 PM。优化管道(也称为中间端)使用新的 PM,而后端目标相关的代码生成使用旧的 PM。

旧的 PM 在某种程度上可以与优化管道配合使用,但这已弃用,并且正在努力删除其使用。

一些 IR Pass 被认为是后端代码生成管道的一部分,即使它们是 LLVM IR Pass(而所有 MIR Pass 都是代码生成 Pass)。这包括通过 TargetPassConfig 钩子添加的任何内容,例如 TargetPassConfig::addCodeGenPrepare()

已删除了用于根据目标扩展旧 PM 的 Pass 的 TargetMachine::adjustPassManager() 函数。它主要用于 opt,但由于 opt 中已删除对使用默认管道的支持,因此该函数不再需要。在新 PM 中,此类调整是通过使用 TargetMachine::registerPassBuilderCallbacks() 来完成的。

目前正在努力使代码生成管道与新的 PM 协同工作。