使用新的Pass管理器

概述

有关新的Pass管理器的概述,请参阅博客文章

请告诉我如何使用新的Pass管理器运行默认优化管道

// 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管理器添加Pass

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

要向新的 PM pass 管理器添加 pass,重要的是匹配 pass 类型和 pass 管理器类型。例如,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 管理器也是该类型的 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 管理器中,而不是为每个 pass 添加适配器到包含的更高级别的 pass 管理器。例如,

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 管理器的典型方法是使用 PassBuilder 并调用类似 PassBuilder::buildPerModuleDefaultPipeline() 的方法,而不是手动向 pass 管理器添加 pass,这将为给定的优化级别创建典型的管道。

有时,前端或后端会希望将 pass 注入到管道中。例如,前端可能想要添加 instrumentation,而目标后端可能想要添加降低自定义 intrinsic 的 pass。对于这些情况,PassBuilder 公开了回调,允许将 pass 注入到管道的某些部分。例如,

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

将在由该 PassBuilder 创建的 pass 管理器的管道的最开始附近添加 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 管理器具有缓存分析并在可能的情况下重用它们的基础设施。

当 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 作为典型的 FunctionAnalysisManager,函数 pass 将有权访问它。要访问外部级别 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 触发外部级别分析计算可能会导致不确定性。相关的限制是,使用的外部级别 IR 分析必须是不可变的,否则它们可能会因内部级别 IR 的更改而失效。内部 pass 未使用的外部分析可以并且经常会被内部级别 IR 的更改而失效。这些失效发生在内部 pass 管理器完成后,因此访问可变分析将给出无效结果。

无法访问外部级别分析的例外情况是在循环 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

为了确保名为 foo 的分析在 pass 之前可用,请将 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()

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

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