LLVM パスの作成

はじめに — パスとは何か?

警告

このドキュメントでは、新しいパスマネージャについて説明します。LLVM はコード生成パイプラインにレガシーパスマネージャを使用します。詳細については、LLVM パスの作成 (レガシー PM バージョン) および 新しいパスマネージャの使用 を参照してください。

LLVM パスフレームワークは LLVM システムの重要な部分です。なぜなら、LLVM パスはコンパイラの興味深い部分のほとんどが存在する場所だからです。パスは、コンパイラを構成する変換と最適化を実行し、これらの変換で使用される分析結果を構築し、何よりもコンパイラコードの構造化技術です。

パスインターフェイスが継承によって定義されるレガシーパスマネージャのパスとは異なり、新しいパスマネージャのパスはコンセプトベースのポリモーフィズムに依存しています。つまり、明示的なインターフェイスはありません (PassManager.h のコメントを参照)。すべての LLVM パスは、CRTP ミックスイン PassInfoMixin<PassT> から継承します。パスには、PreservedAnalyses を返し、分析マネージャとともに IR の単位を受け入れる run() メソッドが必要です。たとえば、関数パスには PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM); メソッドがあります。

まず、パスの構築方法を、ビルドの設定、パスの作成、実行とテストから示します。既存のパスを見ることは、詳細を学ぶ上で常に良い方法です。

クイックスタート — Hello World を書く

ここでは、パスの「Hello World」の作成方法について説明します。「HelloWorld」パスは、コンパイルされているプログラムに存在する非外部関数の名前を出力するように設計されています。プログラムを一切変更せず、単に検査するだけです。

以下のコードは既に存在します。「HelloWorld」ソースファイルと並んで、異なる名前のパスを自由に作成してください。

ビルドの設定

まず、LLVM システムの入門 で説明されているように、LLVM を構成およびビルドします。

次に、既存のディレクトリを再利用します (新しいディレクトリを作成するには、必要以上の CMake ファイルをいじる必要があります)。この例では、すでに作成されている llvm/lib/Transforms/Utils/HelloWorld.cpp を使用します。独自のパスを作成する場合は、llvm/lib/Transforms/Utils/CMakeLists.txt に新しいソースファイルを追加します (パスを Transforms/Utils ディレクトリに配置する場合)。

新しいパスのビルドを設定したので、パス自体のコードを記述する必要があります。

必要な基本コード

新しいパスのビルドを設定したので、あとはそれを記述するだけです。

まず、ヘッダーファイルにパスを定義する必要があります。llvm/include/llvm/Transforms/Utils/HelloWorld.h を作成します。ファイルには、次のボイラープレートが含まれている必要があります。

#ifndef LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
#define LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

#include "llvm/IR/PassManager.h"

namespace llvm {

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};

} // namespace llvm

#endif // LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

これにより、実際にパスを実行する run() メソッドの宣言とともに、パスのクラスが作成されます。PassInfoMixin<PassT> から継承することで、自分で記述する必要のないボイラープレートがさらに設定されます。

クラスは、グローバル名前空間を汚染しないように llvm 名前空間にあります。

次に、llvm/lib/Transforms/Utils/HelloWorld.cpp を作成します。最初は

#include "llvm/Transforms/Utils/HelloWorld.h"

… 作成したヘッダーファイルを含めるためです。

using namespace llvm;

… は、include ファイルの関数が llvm 名前空間にあるため必要です。これはヘッダーファイル以外でのみ行う必要があります。

次に、パスの run() 定義があります。

PreservedAnalyses HelloWorldPass::run(Function &F,
                                      FunctionAnalysisManager &AM) {
  errs() << F.getName() << "\n";
  return PreservedAnalyses::all();
}

… これは、関数の名前を stderr に出力するだけです。パスマネージャは、モジュール内のすべての関数でパスが実行されるようにします。PreservedAnalyses の戻り値は、関数を変更しなかったため、すべての分析 (ドミネータツリーなど) がこのパスの後も有効であることを示します。

これでパス自体は完了です。次に、パスを「登録」するために、いくつかの場所に追加する必要があります。FUNCTION_PASS セクションの llvm/lib/Passes/PassRegistry.def に以下を追加します。

FUNCTION_PASS("helloworld", HelloWorldPass())

… これにより、パスが「helloworld」という名前で追加されます。

llvm/lib/Passes/PassRegistry.def は、さまざまな理由で llvm/lib/Passes/PassBuilder.cpp に複数回 #include されます。パスを構築するため、llvm/lib/Passes/PassBuilder.cpp に適切な #include も追加する必要があります。

#include "llvm/Transforms/Utils/HelloWorld.h"

これでパスに必要なコードはすべて揃いました。次はコンパイルして実行します。

opt でパスを実行する

新しいピカピカのパスができたので、opt をビルドして、それを使用していくつかの LLVM IR をパスに通すことができます。

$ ninja -C build/ opt
# or whatever build system/build directory you are using

$ cat /tmp/a.ll
define i32 @foo() {
  %a = add i32 2, 3
  ret i32 %a
}

define void @bar() {
  ret void
}

$ build/bin/opt -disable-output /tmp/a.ll -passes=helloworld
foo
bar

パスが実行され、関数名が期待どおりに出力されました。

パスをテストする

将来の回帰を防ぐために、パスをテストすることは重要です。llvm/test/Transforms/Utils/helloworld.ll に lit テストを追加します。テストの詳細については、LLVM テストインフラストラクチャガイド を参照してください。

$ cat llvm/test/Transforms/Utils/helloworld.ll
; RUN: opt -disable-output -passes=helloworld %s 2>&1 | FileCheck %s

; CHECK: {{^}}foo{{$}}
define i32 @foo() {
  %a = add i32 2, 3
  ret i32 %a
}

; CHECK-NEXT: {{^}}bar{{$}}
define void @bar() {
  ret void
}

$ ninja -C build check-llvm
# runs our new test alongside all other llvm lit tests

FAQ

必須パス

true を返す静的 isRequired() メソッドを定義するパスは、必須パスです。例:

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);

  static bool isRequired() { return true; }
};

必須パスとは、スキップできないパスのことです。必須パスの例は AlwaysInlinerPass であり、alwaysinline のセマンティクスを維持するために常に実行する必要があります。パスマネージャには、他の必須パスが含まれている可能性があるため、必須です。

パスがスキップできる例としては、optnone 関数属性があります。これは、関数で最適化を実行しないように指定します。必須パスは、optnone 関数でも実行されます。

実装の詳細については、PassInstrumentation::runBeforePass() を参照してください。

パスをプラグインとして登録する

LLVM は、clangopt などのさまざまなツール内にパスプラグインを登録するメカニズムを提供します。パスプラグインは、デフォルトの最適化パイプラインにパスを追加したり、opt などのツールで手動で実行したりできます。詳細については、新しいパスマネージャの使用 を参照してください。

他のプロジェクトと一緒にリポジトリのルートに CMake プロジェクトを作成します。このプロジェクトには、次の最小限の CMakeLists.txt が含まれている必要があります。

add_llvm_pass_plugin(MyPassName source.cpp)

CMake の詳細については、add_llvm_pass_plugin の定義を参照してください。

パスは、新しいパスマネージャ用に少なくとも 2 つのエントリポイント、1 つは静的登録用、もう 1 つは動的にロードされたプラグイン用に提供する必要があります。

  • llvm::PassPluginLibraryInfo get##Name##PluginInfo();

  • extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() LLVM_ATTRIBUTE_WEAK;

パスプラグインは、デフォルトでは動的にコンパイルおよびリンクされます。LLVM_${NAME}_LINK_INTO_TOOLSON に設定すると、プロジェクトは静的にリンクされた拡張機能になります。

インツリーの例については、llvm/examples/Bye/ を参照してください。

PassBuilder に静的にリンクされたパスプラグインを認識させるには

// Declare plugin extension function declarations.
#define HANDLE_EXTENSION(Ext) llvm::PassPluginLibraryInfo get##Ext##PluginInfo();
#include "llvm/Support/Extension.def"

...

// Register plugin extensions in PassBuilder.
#define HANDLE_EXTENSION(Ext) get##Ext##PluginInfo().RegisterPassBuilderCallbacks(PB);
#include "llvm/Support/Extension.def"

PassBuilder に動的にリンクされたパスプラグインを認識させるには

// Load plugin dynamically.
auto Plugin = PassPlugin::Load(PathToPlugin);
if (!Plugin)
  report_error();
// Register plugin extensions in PassBuilder.
Plugin.registerPassBuilderCallbacks(PB);