並列コンパイル
現在では並列化されています。
- codegen 部分はデフォルトで並行実行されます。
-C codegen-units=nオプションを使用して、並行タスクの数を制御できます。 - 型チェック、借用チェック、mir 最適化など、HIR lowering から codegen までの部分は
nightly 版で並列化されています。
現在、これらはデフォルトでは直列に実行され、並列化は
-Z threads = nオプションを使用してユーザーが手動で有効にします。 - 字句解析、HIR lowering、マクロ展開などの他の部分は、 まだ直列モードで実行されています。
コード生成
単相化中、コンパイラは生成されるすべてのコードを
codegen units と呼ばれるより小さなチャンクに分割します。
これらは並列に実行される LLVM の独立したインスタンスによって生成されます。
最後に、リンカーが実行され、
すべての codegen units を 1 つのバイナリに結合します。
この処理は rustc_codegen_ssa::base モジュールで行われます。
データ構造
並列コンパイラで使用される基盤となるスレッドセーフなデータ構造は、
rustc_data_structures::sync モジュールにあります。
これらのデータ構造は、
parallel-compiler が true かどうかによって異なる方法で実装されています。
| データ構造 | 並列 | 非並列 |
|---|---|---|
| Lock<T> | (parking_lot::Mutex<T>) | (std::cell::RefCell) |
| RwLock<T> | (parking_lot::RwLock<T>) | (std::cell::RefCell) |
| ReadGuard | parking_lot::RwLockReadGuard | std::cell::Ref |
| MappedReadGuard | parking_lot::MappedRwLockReadGuard | std::cell::Ref |
| WriteGuard | parking_lot::RwLockWriteGuard | std::cell::RefMut |
| MappedWriteGuard | parking_lot::MappedRwLockWriteGuard | std::cell::RefMut |
| LockGuard | parking_lot::MutexGuard | std::cell::RefMut |
-
これらのスレッドセーフなデータ構造はコンパイル中に散在しており、 ロック競合を引き起こす可能性があり、その結果、スレッド数が 4 を超えて増えると パフォーマンスが低下します。そのため、これらのデータ構造の使用を監査し、 共有状態の使用を減らすためのリファクタリング、または 不変条件、アトミック性、ロック順序の詳細を網羅する永続的なドキュメントの作成につなげています。
-
一方で、コンパイル中の他のどのような不変条件が 並列コンパイルで成立しない可能性があるのかは、まだ明らかにする必要があります。
WorkerLocal
WorkerLocal は並列コンパイラ向けに実装された特別なデータ構造です。
これはスレッドプール内の各スレッドに対して worker-local な値を保持します。
worker local の値には、
それが構築されたスレッドプール上の Deref impl を通じてのみアクセスできます。
それ以外の場合は panic します。
WorkerLocal は並列環境で Arena アロケータを実装するために使用され、
これは並列クエリにおいて重要です。
その実装は
rustc_data_structures::sync::worker_local モジュールにあります。
ただし、
非並列コンパイラでは、これは (OneThread<T>) として実装され、その T には
Deref::deref を通じて直接アクセスできます。
並列イテレータ
rayon crate によって提供される並列イテレータは、
並列処理を実装するための簡単な方法です。
並列コンパイラの現在の実装では、
タスクを並列に実行するために 独自に fork した rayon を使用しています。
parallel-compiler が true の場合にループを並列に実行するための
いくつかのイテレータ関数が実装されています。
関数(Send と Sync は省略) | 概要 | 所有モジュール |
|---|---|---|
| par_iter<T: IntoParallelIterator>(t: T) -> T::Iter | 並列イテレータを生成する | rustc_data_structure::sync |
| par_for_each_in<T: IntoParallelIterator>(t: T, for_each: impl Fn(T::Item)) | 並列イテレータを生成し、各要素に対して for_each を実行する | rustc_data_structure::sync |
| Map::par_body_owners(self, f: impl Fn(LocalDefId)) | crate 内のすべての hir owner に対して f を実行する | rustc_middle::hir::map |
| Map::par_for_each_module(self, f: impl Fn(LocalDefId)) | crate 内のすべてのモジュールとサブモジュールに対して f を実行する | rustc_middle::hir::map |
| ModuleItems::par_items(&self, f: impl Fn(ItemId)) | モジュール内のすべての item に対して f を実行する | rustc_middle::hir |
| ModuleItems::par_trait_items(&self, f: impl Fn(TraitItemId)) | モジュール内のすべての trait item に対して f を実行する | rustc_middle::hir |
| ModuleItems::par_impl_items(&self, f: impl Fn(ImplItemId)) | モジュール内のすべての impl item に対して f を実行する | rustc_middle::hir |
| ModuleItems::par_foreign_items(&self, f: impl Fn(ForeignItemId)) | モジュール内のすべての foreign item に対して f を実行する | rustc_middle::hir |
コンパイラには、これらの関数を使用して並列化できる可能性のあるループが多数あります。
2022年8月時点で、並列イテレータ関数が使用されているシナリオは次のとおりです。| 呼び出し元 | シナリオ | 呼び出し先 |
|---|---|---|
| rustc_metadata::rmeta::encoder::prefetch_mir | メタデータのエンコードで後から必要になるクエリをプリフェッチする | par_iter |
| rustc_monomorphize::collector::collect_crate_mono_items | 非ジェネリック項目から到達可能な単相化済み項目を収集する | par_for_each_in |
| rustc_interface::passes::analysis | match 文の妥当性をチェックする | Map::par_body_owners |
| rustc_interface::passes::analysis | MIR の borrow check | Map::par_body_owners |
| rustc_typeck::check::typeck_item_bodies | 型チェック | Map::par_body_owners |
| rustc_interface::passes::hir_id_validator::check_crate | hir の妥当性をチェックする | Map::par_for_each_module |
| rustc_interface::passes::analysis | ループ本体、属性、naked 関数、不安定な ABI、const 本体の妥当性をチェックする | Map::par_for_each_module |
| rustc_interface::passes::analysis | MIR の liveness と intrinsic のチェック | Map::par_for_each_module |
| rustc_interface::passes::analysis | 到達不能性のチェック | Map::par_for_each_module |
| rustc_interface::passes::analysis | プライバシーチェック | Map::par_for_each_module |
| rustc_lint::late::check_crate | モジュールごとの lint を実行する | Map::par_for_each_module |
| rustc_typeck::check_crate | well-formedness のチェック | Map::par_for_each_module |
並列イテレーターを使用できる可能性のあるループは、まだ多数あります。
クエリシステム
クエリモデルには、あまり多くの労力をかけずに複数のクエリを並列に評価することを実際に可能にする性質がいくつかあります。
- クエリプロバイダーがアクセスできるすべてのデータはクエリコンテキスト経由であるため、 クエリコンテキストがアクセスの同期を処理できます。
- クエリ結果は不変であることが要求されるため、 異なるスレッドから同時に安全に使用できます。
クエリ foo が評価されると、foo のキャッシュテーブルがロックされます。
- すでに結果がある場合は、それをクローンし、ロックを解放して 完了します。
- キャッシュエントリがなく、同じ結果を計算している他のアクティブなクエリ呼び出しもない場合は、 そのキーを「進行中」としてマークし、ロックを解放して 評価を開始します。
- 同じキーに対する別のクエリ呼び出しが進行中である場合は、 ロックを解放し、待機している結果を別の呼び出しが計算するまで そのスレッドをブロックするだけです。 並列コンパイラにおける循環エラー検出には、 シングルスレッドモードよりも複雑なロジックが必要です。 並列クエリ内のワーカースレッドが相互依存のために進捗しなくなると、 コンパイラは追加のスレッド (deadlock handler という名前) を使用して、 循環エラーを検出、除去、報告します。
並列クエリ機能には、まだ実装すべきことが残っており、その大部分は
前述の Data Structures と Parallel Iterators に関連しています。
この未解決の機能追跡 issueを参照してください。
Rustdoc
2022年11月時点では、`rustdoc` のレンダリングを並列化できるようにするまでに完了すべきステップがまだいくつかあります(並列 rustdoc に関する未解決の議論を参照してください)。
リソース
詳細を学ぶために使用できるリソースをいくつか示します。