バックエンド非依存の Codegen
rustc_codegen_ssa は、すべてのバックエンド、すなわち LLVM、Cranelift、GCC が実装するための抽象インターフェイスを提供します。
以下は、この抽象インターフェイスを作成したリファクタリングに関する背景情報です。
rustc_codegen_llvm のリファクタリング
Denis Merigoux 著、2018年10月23日
リファクタリング前のコードの状態
MIR から LLVM IR へのコンパイルに関連するすべてのコードは、
rustc_codegen_llvm クレート内に含まれていました。
最も重要な要素の内訳は次のとおりです。
backフォルダー(7,800 LOC)は、LLVM を介してさまざまなオブジェクトファイルとアーカイブを作成するメカニズムに加え、並列コード生成のための通信メカニズムを実装しています。debuginfo(3,200 LOC)フォルダーには、デバッグ情報を LLVM に渡すためのすべてのコードが含まれています。llvm(2,200 LOC)フォルダーは、C++ API を使用して LLVM と通信するために必要な FFI を定義しています。mir(4,300 LOC)フォルダーは、MIR から LLVM IR への実際の lowering を実装しています。base.rs(1,300 LOC)ファイルには、いくつかのヘルパー関数に加えて、コード生成を起動し、作業を分配する高レベルのコードが含まれています。builder.rs(1,200 LOC)ファイルには、基本ブロック内で個々の LLVM IR 命令を生成するすべての関数が含まれています。common.rs(450 LOC)には、さまざまなヘルパー関数と、LLVM の静的値を生成するすべての関数が含まれています。type_.rs(300 LOC)は、LLVM IR への型変換の大部分を定義しています。
このリファクタリングの目的は、このクレート内で、LLVM 固有のコードと、他の rustc バックエンドで再利用できるコードを分離することです。
たとえば、
mir フォルダーはほぼ完全にバックエンド固有ですが、
クレートの他の部分に大きく依存しています。
コードの分離は、コードのロジックにも、
その性能にも影響してはなりません。
これらの理由から、分離プロセスでは、結果のコードをコンパイル可能にするために、同時に行う必要がある 2 つの変換が伴います。
- 関数シグネチャと構造体定義内のすべての LLVM 固有の型をジェネリクスに置き換える
- LLVM FFI を呼び出すすべての関数を、バックエンド非依存コードとバックエンドの間のインターフェイスを定義する一連のトレイト内にカプセル化する
LLVM 固有のコードは rustc_codegen_llvm に残される一方で、すべての新しいトレイトとバックエンド非依存コードは rustc_codegen_ssa(@eddyb による名称提案)に移動されます。
ジェネリックな型と構造体
@irinagpopa は、LLVM では参照 &'ll Value として実装されるジェネリックな Value 型によって、rustc_codegen_llvm の型をパラメータ化し始めました。
この作業は、mir フォルダー内およびその他の場所にあるすべての構造体に加え、LLVM の BasicBlock 型と Type 型にも拡張されました。
LLVM codegen にとって最も重要な 2 つの構造体は CodegenCx と Builder です。
これらは、複数のライフタイムパラメータと Value の型によってパラメータ化されています。
struct CodegenCx<'ll, 'tcx> {
/* ... */
}
struct Builder<'a, 'll, 'tcx> {
cx: &'a CodegenCx<'ll, 'tcx>,
/* ... */
}
CodegenCx は、複数の関数を含むことができる 1 つの codegen-unit をコンパイルするために使用される一方、Builder は 1 つの基本ブロックをコンパイルするために作成されます。
rustc_codegen_llvm のコードは、複数の明示的なライフタイムパラメータを扱う必要があります。これらは次のものに対応します。
'tcxは最も長いライフタイムで、プログラムの情報を含む元のTyCtxtに対応します。'aは、構造体内のCodegenCxまたは別のオブジェクトの短命な参照です。'llは、ValueやTypeなどの LLVM オブジェクトへの参照のライフタイムです。
コードにはすでに多くのライフタイムパラメータがありますが、ジェネリック化したことで、操作される LLVM オブジェクトの特殊な性質(それらは extern ポインタです)によってのみ borrow-checker が通過していた状況が明らかになりました。
たとえば、analyse.rs の LocalAnalyser に追加のライフタイムパラメータを追加する必要があり、次の定義になりました。
struct LocalAnalyzer<'mir, 'a, 'tcx> {
/* ... */
}
しかし、最も重要な 2 つの構造体である
CodegenCx と Builder は、
バックエンド非依存コードでは定義されていません。
実際、それらの内容はバックエンドに非常に固有であり、バックエンドのコンテキスト用にジェネリックフィールドを介して狭い余地だけを許すよりも、それらの定義をバックエンド実装者に任せるほうが理にかなっています。
トレイトとインターフェイス
CodegenCx と Builder はバックエンドによって定義される必要があるため、
バックエンドのインターフェイスを定義するすべてのトレイトを実装する構造体になります。
これらのトレイトは rustc_codegen_ssa/traits フォルダーで定義され、すべてのバックエンド非依存コードはそれらによってパラメータ化されます。
たとえば、base.rs の関数がどのようにパラメータ化されるかを説明します。
pub fn codegen_instance<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
cx: &'a Bx::CodegenCx,
instance: Instance<'tcx>
) {
/* ... */
}
このシグネチャには、前に説明した 2 つのライフタイムパラメータと、Builder 構造体が満たすインターフェイスに対応する BuilderMethods トレイトを満たすマスター型 Bx があります。
BuilderMethods は関連型 Bx::CodegenCx を定義しており、それ自体が構造体 CodegenCx によって実装される CodegenMethods トレイトを満たします。
トレイト側について、traits/builder.rs にある BuilderMethods の定義の一部を例として示します。
pub trait BuilderMethods<'a, 'tcx>:
HasCodegen<'tcx>
+ DebugInfoBuilderMethods<'tcx>
+ ArgTypeMethods<'tcx>
+ AbiBuilderMethods<'tcx>
+ IntrinsicCallMethods<'tcx>
+ AsmBuilderMethods<'tcx>
{
fn new_block<'b>(
cx: &'a Self::CodegenCx,
llfn: Self::Function,
name: &'b str
) -> Self;
/* ... */
fn cond_br(
&mut self,
cond: Self::Value,
then_llbb: Self::BasicBlock,
else_llbb: Self::BasicBlock,
);
/* ... */
}
最後に、ExtraBackendMethods トレイトを実装するマスター構造体が、base.rs の codegen_crate のような高レベルの codegen 駆動関数に使用されます。
LLVM では、それは空の LlvmCodegenBackend です。
ExtraBackendMethods は、rustc_codegen_ssa/src/traits/backend.rs で定義されている CodegenBackend を実装する構造体と同じ構造体によって実装されるべきです。
トレイト化のプロセス中に、一部の関数はローカル構造体のメソッドから CodegenCx または Builder のメソッドに変換され、対応する self パラメータが追加されました。
実際、LLVM は内部的に情報を格納しており、API 経由で呼び出されたときにそれへアクセスできます。
これらのメソッドが呼び出される際に持ち回られる Rust データ構造には、この情報は現れません。
しかし、rustc 用の Rust バックエンドを実装する場合、これらのメソッドは CodegenCx からの情報を必要とするため、追加のパラメータが必要になります(トレイトの LLVM 実装では未使用です)。
リファクタリング後のコードの状態
トレイトは、LLVM の API と非常によく似た API を提供します。 これは最善の解決策ではありません。LLVM には非常に特殊なやり方があるためです。 別のバックエンドを追加する際には、 より高い柔軟性を提供するためにトレイト定義が変更される可能性があります。
しかし、バックエンド非依存のコードと LLVM 固有のコードとの現在の分離により、
古い rustc_codegen_llvm のかなりの部分を再利用できました。
以下は、最も重要な要素についての、バックエンド非依存(BA)と LLVM の
新しい LOC 内訳です。
backフォルダー: 3,800(BA)対 4,100(LLVM);mirフォルダー: 4,400(BA)対 0(LLVM);base.rs: 1,100(BA)対 250(LLVM);builder.rs: 1,400(BA)対 0(LLVM);common.rs: 350(BA)対 350(LLVM);
debuginfo フォルダーは分割による影響をほとんど受けず、LLVM 固有のままです。
その高レベル機能だけがトレイト化されています。
新しい traits フォルダーには、トレイト定義だけで 1500 LOC あります。
全体として、
27,000 LOC 規模だった古い rustc_codegen_llvm のコードは、新しい
18,500 LOC 規模の新しい rustc_codegen_llvm と、12,000 LOC 規模の rustc_codegen_ssa に分割されました。
このリファクタリングにより、そうでなければ rustc の複数のバックエンド間で
重複させる必要があったであろう
約 10,000 LOC を再利用できたと言えます。
リファクタリングされた rustc のバックエンドは、
テストスイートにおいても性能ベンチマークにおいても回帰を引き起こしませんでした。これは、
コンパイル時パラメトリシティのみを使用した(トレイトオブジェクトを使用しない)
リファクタリングの性質と一致しています。