使用新的 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,請參閱此頁面。
要將 pass 新增至新的 PM 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)));
將在模組中的第一個函數上執行 FunctionPass1
和 FunctionPass2
,然後在模組中的第二個函數上執行這兩個 pass,依此類推。 這對於 LLVM 資料結構周圍的快取局部性更好。 這同樣適用於其他 IR 類型,並且在某些情況下甚至會影響最佳化品質。 例如,在迴圈上執行所有迴圈 pass 可能會使稍後的迴圈比單獨執行每個迴圈 pass 更容易被最佳化。
將 Pass 插入預設流程¶
建立 pass 管理器的典型方法是使用 PassBuilder
並調用類似 PassBuilder::buildPerModuleDefaultPipeline()
的方法,而不是手動將 pass 新增至 pass 管理器,這會為給定的最佳化層級建立典型的流程。
有時前端或後端會希望將 pass 注入到流程中。 例如,前端可能想要新增檢測,而目標後端可能想要新增降低自訂內建函數的 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);
通過 getCachedResult()
請求快取和不可變的外部層級 IR 分析有效,但不允許直接訪問外部層級 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 想要保持某些特定分析的最新狀態,例如在更新它比使其失效和重新計算更快時,則分析本身可能具有針對特定轉換更新它的方法,或者可能存在類似於 DominatorTree
的 DomTreeUpdater
的輔助更新器。 否則,要將某些分析標記為不再有效,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 之前已為所有函數計算了某些函數分析
$ 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 一起運作。