使用新的 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 加入 Pass Manager

如需如何編寫新的 PM Pass,請參閱本頁

若要將 Pass 加入新的 PM Pass Manager,重要的是要匹配 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 階層為模組 -> (CGSCC ->) 函數 -> 迴圈,其中是否經過 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,然後在模組的第二個函數上執行這兩個遍歷,依此類推。這有利於 LLVM 資料結構周圍的快取區域性。這同樣適用於其他 IR 類型,並且在某些情況下甚至會影響最佳化的品質。例如,對迴圈執行所有迴圈遍歷可能會導致後續迴圈能夠比單獨執行每個迴圈遍歷進行更多最佳化。

將遍歷插入預設流程

建立遍歷管理器的典型方法不是手動將遍歷新增到遍歷管理器,而是使用 PassBuilder 並呼叫類似 PassBuilder::buildPerModuleDefaultPipeline() 的函數,它會為給定的最佳化級別建立一個典型的流程。

有時前端或後端會想要將遍歷插入流程中。例如,前端可能想要新增檢測,而目標後端可能想要新增降低自訂內建函數的遍歷。對於這些情況,PassBuilder 公開了回呼,允許將遍歷插入流程的某些部分。例如,

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

會在由該 PassBuilder 建立的遍歷管理器的流程的最開始附近新增 FooPass。有關可以新增遍歷的各個位置,請參閱 PassBuilder 的文件。

如果 PassBuilder 具有後端對應的 TargetMachine,它將呼叫 TargetMachine::registerPassBuilderCallbacks() 以允許後端將遍歷插入流程中。

Clang 的 BackendUtil.cpp 顯示了前端將(主要是消毒程式)遍歷新增到流程的各個部分的範例。AMDGPUTargetMachine::registerPassBuilderCallbacks() 是後端將遍歷新增到流程的各個部分的範例。

遍歷插件也可以將遍歷新增到預設流程中。不同的工具有不同的載入動態遍歷插件的方法。例如,opt -load-pass-plugin=path/to/plugin.so 將遍歷插件載入 opt。有關編寫遍歷插件的資訊,請參閱編寫 LLVM 遍歷

使用分析

LLVM 提供了許多遍歷可以使用分析,例如支配樹。計算這些分析的成本可能很高,因此新的遍歷管理器具有快取分析並在可能的情況下重複使用它們的基礎結構。

當遍歷在某些 IR 上執行時,它還會收到一個分析管理器,它可以使用該管理器查詢分析。查詢分析將導致管理器檢查它是否已經計算了請求的 IR 的結果。如果它已經有並且結果仍然有效,它將返回該結果。否則,它將通過呼叫分析的 run() 方法構造一個新結果,將其快取並返回。您也可以要求分析管理器僅在分析已經快取時才返回它。

分析管理器僅針對傳遞運行的相同 IR 類型提供分析結果。例如,函數傳遞會接收僅提供函數級別分析的分析管理器。這適用於許多在固定範圍內運作的傳遞。但是,某些傳遞希望向上或向下窺視 IR 階層。例如,SCC 傳遞可能希望查看 SCC 內函數的函數分析。或者它可能希望查看一些不可變的全局分析。在這些情況下,分析管理器可以提供外部或內部級別分析管理器的代理。例如,要從 CGSCCAnalysisManager 獲取 FunctionAnalysisManager,您可以調用

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

並將 FAM 用作函數傳遞可以訪問的典型 FunctionAnalysisManager。要訪問外部級別 IR 分析,您可以調用

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

通過 getCachedResult() 請求緩存且不可變的外部級別 IR 分析是可行的,但不允許直接訪問外部級別 IR 分析管理器來計算外部級別 IR 分析。這有幾個原因。

第一個原因是在內部級別 IR 傳遞中跨外部級別 IR 運行分析可能會導致二次編譯時間行為。例如,模組分析通常會掃描每個函數,而允許函數傳遞運行模組分析可能會導致我們以二次方次數掃描函數。如果傳遞可以保持外部級別分析為最新狀態,而不是按需計算它們,這就不是問題,但是要確保每個傳遞都更新所有外部級別分析將會有很多工作,而到目前為止這還沒有必要,而且也沒有基礎設施來實現這一點(除了下面描述的循環傳遞中的函數分析)。優雅地降級的自我更新分析也能夠處理這個問題(例如 GlobalsAA),但是如果我們想要精度,它們會遇到必須在優化流程中的某個地方手動重新計算的問題,並且它們會阻止未來潛在的併發性。

第二個原因是要牢記未來潛在的傳遞併發性,例如在 CGSCC 或模組中的不同函數上並行化函數傳遞。由於傳遞可以請求緩存的分析結果,因此如果支持併發性,則允許傳遞觸發外部級別分析計算可能會導致不確定性。一個相關的限制是,必須使用不可變的外部級別 IR 分析,否則它們可能會因內部級別 IR 的更改而失效。內部傳遞未使用的外部分析可以並且通常會因內部級別 IR 的更改而失效。這些失效發生在內部傳遞管理器完成之後,因此訪問可變分析將會給出無效的結果。

無法訪問外部級別分析的例外情況是在循環傳遞中訪問函數分析。循環傳遞經常使用函數分析,例如支配樹。循環傳遞本身就需要修改循環所在的函數,這包括循環分析所依賴的一些函數分析。這會降低函數中不同循環的未來併發性,但這是由於循環及其函數的緊密耦合而產生的權衡。為了確保循環傳遞使用的函數分析有效,會在循環傳遞中手動更新它們,以確保不需要失效。循環傳遞和分析可以訪問一組通用的函數分析,這些分析作為 LoopStandardAnalysisResults 參數傳遞到循環傳遞中。其他可變函數分析無法從循環傳遞中訪問。

如同任何快取機制,我們需要某種方式來告知分析管理器何時結果不再有效。分析管理器的大部分複雜性來自於試圖使盡可能少的分析結果失效,以盡可能保持編譯時間的低廉。

處理可能無效的分析結果有兩種方法。一種是簡單地強制清除結果。這通常僅應在結果所依據的 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);
}

失效與分析管理器代理的結合導致了一些複雜性。例如,當我們使模組傳遞中的所有分析失效時,我們必須確保也使通過任何現有的內部代理可訪問的函數分析失效。內部代理的 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 通常需要顯式的傳遞嵌套。例如,要運行函數傳遞,然後運行模組傳遞,我們需要將函數傳遞包裝在模組適配器中

$ 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 嵌套是可選的。

為了便於輸入,有一些特殊情況

  • 如果第一個傳遞不是模組傳遞,則會隱式創建第一個傳遞的傳遞管理器

    • 例如,以下兩者是等效的

$ 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
  • 如果存在一個傳遞的適配器,使其可以放入先前的傳遞管理器中,則會隱式創建該適配器

    • 例如,以下兩者是等效的

$ 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

有關可用傳遞和分析的列表,包括它們運行的 IR 單元(模組、CGSCC、函數、迴圈),請運行

$ opt --print-passes

或查看 PassRegistry.def

為了確保在傳遞之前可以使用名為 foo 的分析,請在傳遞管道中添加 require<foo>。這會添加一個僅請求運行分析的傳遞。此傳遞也需要正確嵌套。例如,要確保在模組傳遞之前已為所有函數計算了一些函數分析

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

新版和舊版傳遞管理器的狀態

LLVM 目前包含兩個傳遞管理器,舊版 PM 和新版 PM。優化管道(也稱為中間端)使用新版 PM,而後端目標相關代碼生成使用舊版 PM。

舊版 PM 在一定程度上可以與優化管道一起使用,但這已被棄用,並且目前正在努力移除其使用。

某些 IR 階段被視為後端程式碼生成管線的一部分,即使它們是 LLVM IR 階段(而所有 MIR 階段都是程式碼生成階段)。這包括透過 TargetPassConfig 掛鉤添加的任何內容,例如 TargetPassConfig::addCodeGenPrepare()

過去用於根據目標擴展傳統 PM 階段的 TargetMachine::adjustPassManager() 函數已被移除。它主要用於 opt,但由於 opt 中不再支援使用預設管線,因此不再需要該函數。在新 PM 中,此類調整是透過使用 TargetMachine::registerPassBuilderCallbacks() 完成的。

目前正在努力使程式碼生成管線與新 PM 一起使用。