新しいパス マネージャーの使用¶
概要¶
新しいパス マネージャーの概要については、ブログ記事を参照してください。
新しいパス マネージャーでデフォルトの最適化パイプラインを実行する方法を教えてください¶
// 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
を参照してください。
パス マネージャーへのパスの追加¶
新しい PM パスを作成する方法については、このページを参照してください。
新しい PM パス マネージャーにパスを追加する場合、重要なことは、パスの型とパス マネージャーの型を一致させることです。たとえば、FunctionPassManager
には関数パスのみを含めることができます。
FunctionPassManager FPM;
// InstSimplifyPass is a function pass
FPM.addPass(InstSimplifyPass());
関数内のすべてのループで実行されるループ パスを FunctionPassManager
に追加する場合は、ループ パスを関数パス アダプターでラップする必要があります。これは、関数内のすべてのループを調べて、各ループでループ パスを実行します。
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 ユニットのパス マネージャーもその種類のパスです。たとえば、FunctionPassManager
は関数パスです。つまり、ModulePassManager
に追加できます。
ModulePassManager MPM;
FunctionPassManager FPM;
// InstSimplifyPass is a function pass
FPM.addPass(InstSimplifyPass());
MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
一般に、CGSCC/関数/ループ パスをパス マネージャーにグループ化することをお勧めします。パスごとに上位レベルのパス マネージャーにアダプターを追加するのではなく、たとえば、
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
を実行し、次にモジュール内の 2 番目の関数で両方のパスを実行します。以下同様です。これは、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);
キャッシュされ変更不可能な外側のレベルの IR 解析の要求は、getCachedResult()
を介して機能しますが、外側のレベルの IR 解析を計算するための外側のレベルの IR 解析マネージャーへの直接アクセスは許可されていません。これにはいくつかの理由があります。
最初の理由は、内側のレベルの IR パスで外側のレベルの IR 全体で解析を実行すると、コンパイル時間が二次的な動作になる可能性があることです。たとえば、モジュール解析はすべての関数をスキャンすることが多く、関数パスがモジュール解析を実行できるようにすると、関数のスキャンが二次回数になる可能性があります。パスが要求時に計算するのではなく、外側のレベルの解析を最新の状態に保つことができれば、これは問題にはなりませんが、すべてのパスがすべての外側のレベルの解析を更新するようにするのは大変な作業であり、これまでは必要なく、これに対するインフラストラクチャもありません(以下に説明するように、ループ パスの関数解析を除く)。正常に機能しなくなる自己更新解析(たとえば、GlobalsAA)もこの問題を処理しますが、精度が必要な場合は最適化パイプラインのどこかで手動で再計算する必要があるという問題に直面し、将来の並行処理を妨げます。
2 番目の理由は、たとえば、CGSCC またはモジュール内のさまざまな関数で関数パスを並列化するなど、将来のパスの並行処理を考慮することです。パスはキャッシュされた解析結果を要求できるため、パスが外側のレベルの解析の計算をトリガーすると、並行処理がサポートされている場合に非決定論が生じる可能性があります。関連する制限として、使用される外側のレベルの IR 解析は変更できない必要があり、そうでない場合は内側のレベルの IR への変更によって無効になる可能性があります。内側のパスで使用されていない外側の解析は、内側のレベルの IR への変更によって無効になることがよくあります。これらの無効化は内側のパス マネージャーが終了した後に発生するため、変更可能な解析にアクセスすると無効な結果が得られます。
外側のレベルの解析にアクセスできない例外は、ループ パスで関数解析にアクセスする場合です。ループ パスは、ドミネーター ツリーなどの関数解析をよく使用します。ループ パスは、本質的にループがある関数を変更する必要があり、それにはループ解析が依存する関数解析の一部が含まれます。これは、関数内の別々のループに対する将来の並行処理を考慮していませんが、それはループとその関数が密接に結び付いているため、トレードオフになります。ループ パスが使用する関数解析が有効であることを確認するために、無効化が必要ないように、ループ パスで手動で更新されます。ループ パスと解析がアクセスできる一連の共通関数解析があり、LoopStandardAnalysisResults
パラメーターとしてループ パスに渡されます。その他の変更可能な関数解析は、ループ パスからアクセスできません。
あらゆるキャッシュ メカニズムと同様に、結果が有効でなくなったときに解析マネージャーに通知する方法が必要です。解析マネージャーの複雑さの多くは、コンパイル時間を可能な限り低く保つために、可能な限り少ない解析結果を無効にしようとすることから生じています。
無効になる可能性のある解析結果を処理するには、2 つの方法があります。1 つは、単純に結果を強制的にクリアすることです。これは通常、結果のキーとなっている IR が無効になった場合にのみ使用する必要があります。たとえば、関数が削除されたり、呼び出しグラフの変更により CGSCC が無効になったりした場合です。
解析結果を無効にする一般的な方法は、パスが保持する解析の種類と、保持しない解析の種類を宣言することです。IRを変換する際、パスはIR変換と並行して解析を更新するか、解析マネージャーに解析が無効になり、無効化する必要があることを通知するかのいずれかの選択肢を持ちます。パスが、特定の解析を最新の状態に保ちたい場合(更新する方が無効化して再計算するよりも速い場合など)、解析自体に特定の変換に合わせて更新するためのメソッドがあるか、DominatorTree
用のDomTreeUpdater
のようなヘルパーアップデーターがある場合があります。そうでない場合は、特定の解析を無効としてマークするために、パスは適切な解析が無効化された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;
パスマネージャーは、パスから返されたPreservedAnalyses
を使って、解析マネージャーのinvalidate()
メソッドを呼び出します。これはパス内から手動で行うこともできます。
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解析にアクセスする際に注意すべきことの1つは、削除されたIRのキャッシュされた結果です。モジュールパスで関数が削除された場合、そのアドレスはキャッシュされた解析のキーとして引き続き使用されます。パス内で、その関数の結果をクリアするか、内部解析を一切使用しないように注意してください。
AM.invalidate(M, PreservedAnalyses::none());
は、内部解析マネージャープロキシを無効化し、キャッシュされたすべての解析をクリアします。これは、キャッシュされた解析のキーとして無効なアドレスが使用されていると保守的に仮定しているためです。ただし、どの解析をキャッシュ/無効化するかをより選択的に行いたい場合は、解析マネージャープロキシを保持済みにマークできます。これは、削除されたすべてのエントリが手動で処理されたと本質的に述べています。これは、すべての適切な解析が無効化されていることを確認するのが難しい可能性があるため、コンパイル時間の測定可能な向上がある場合にのみ行う必要があります。
解析の無効化の実装¶
デフォルトでは、解析は、それが実行されるIRユニットに対する解析がPreservedAnalyses
によって保持されないと示された場合に無効化されます(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の2つのパスマネージャーが含まれています。最適化パイプライン(別名、ミドルエンド)は新しいPMを使用しますが、バックエンドのターゲット依存コード生成はレガシーPMを使用します。
レガシーPMは最適化パイプラインである程度機能しますが、これは非推奨であり、その使用を削除するための取り組みが進行中です。
一部のIRパスは、LLVM IRパスであっても、バックエンドコード生成パイプラインの一部と見なされます(すべてのMIRパスがコード生成パスである一方)。これには、TargetPassConfig
フック(例:TargetPassConfig::addCodeGenPrepare()
)を介して追加されたものがすべて含まれます。
ターゲットごとにパスを使用してレガシーPMを拡張するために使用されていたTargetMachine::adjustPassManager()
関数は削除されました。これは主にoptから使用されていましたが、optでのデフォルトパイプラインの使用のサポートが削除されたため、この関数は不要になりました。新しいPMでは、このような調整はTargetMachine::registerPassBuilderCallbacks()
を使用して行われます。
現在、コード生成パイプラインを新しいPMで動作させるための取り組みが行われています。