
2026/01/12 23:18
LLVM:悪い点
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
LLVM の設計、レビュー プロセス、およびツールは、コントリビューターとユーザーに大きな摩擦を生じさせます。レビュアーが過負荷になっているため、新しい著者は資格のあるレビュアーを見つけられず、彼らのプルリクエストは専門知識を欠く同僚によってローブール・スタンプされてしまいます。コントリビューション モデルでは、著者がレビュアーを要求する必要があり、新参者にとって障壁となります。C++ API と IR は不安定であり、頻繁な変更は下流のバックエンド(コンパイラ フロントエンドやランタイム)がコードを書き換え続けることを強いられ、保守コストが膨張します。
ビルド時間は 2.5 百万行以上の C++ を超えており、低スペックハードウェアでのコンパイルが困難です。特にデバッグ ビルドでは OOM のリスクがあり、大きなディスク空間を消費します。プリコンパイルヘッダー、デフォルト dylib ビルド、およびデーモン化されたテストはビルド時間を短縮できますが、CI は決して完全に緑色になりません—フラッキーなテスト(例:lldb/OpenMP)と不安定なビルドボットは偽陽性を生成し、本当の失敗の信号を希薄化します。エンドツーエンド テストカバレッジは散漫です;LLVM 自体の lit テストには実行可能チェックが欠けており、llvm-test-suite は float / integer / vector サイズにわたる数少ない操作しかカバーしていません。
バックエンドの発散は続いています:ターゲット固有の DAG 組み合わせと最適化フックは重複を生み出し、クロスターゲットの一貫性を妨げます。コンパイル時間は最近の改善後も高く残っており(特に -O0 で)、TPDE バックエンドは順序の大幅な削減が期待できます。
LLVM には公式の公開パフォーマンス追跡インフラストラクチャが欠如しています。LNT は壊れており UX が悪いため、変更によるランタイムへの影響を評価することが困難です。将来計画としては、不定値、非安全性、および仕様のギャップに対処するための正式な IR スペックグループの設立、ABI ライディング(呼び出し規約)に関する GSoC プロトタイプ、および TPDE バックエンドを研究して実質的なコンパイル時間削減を達成することが挙げられます。
本文
数年前、LLVM IR の設計上の問題点についてブログ記事を書きました。以来、これらのうちいくつかは完全に修正され(opaque‑pointer migration)、もう一つはほぼ解決済み(constant‑expression removal)で、残りの 3 番目は修正への道が進んでいる(ptradd migration)ことが確認されています。
今回はさらに大胆に取り組み、3つだけでは終わらないようにしたいと思います。もちろん、これらの問題はすべて同等に重要というわけではありません。どれほど重要かは聞く相手によります。短さを保つため、主に「何が問題なのか」を説明し、解決策については触れません。
最後に、これは LLVM プロジェクトのリードメンテイナーとしての視点から書いていることを指摘しておきます。LLVM を使うのやめるべき理由ではなく、改善の機会を示したいだけです。
高レベルな課題
レビュー体制(Review capacity)
多くのオープンソースプロジェクトと異なり、LLVM は 貢献者が不足しているわけではありません。何千人もの貢献者がおり、分布も比較的フラットです(つまり、少数精鋭に大部分の寄与を任せるような構造ではありません)。
しかし LLVM が直面している問題は「レビュー体制が不十分」という点です。コードを書いてくれる人よりも、レビューを行う人が格段に不足しています。コードレビューには書くこと以上の専門知識が必要であり、レビュアー本人やその雇用主にとって即座に価値を生むものではない場合があります。
レビュー体制の欠如は貢献者エクスペリエンスを悪化させるだけでなく、不適切な変更がコードベースに入り込む原因にもなります。典型的なパターンは以下の通りです:
- 何か PR を作成する。
- 長時間、十分なレビューが得られないまま進行する。
- 該当領域の正式なレビュアーではない同僚が最終承認を与える。
LLVM の貢献モデルは特異です。PR 作成者がレビュアーを「リクエスト」する責任があります。これは新規貢献者にとって特に問題で、誰にリクエストすればよいか分からないケースが多いです。関連するレビュアーはラベルベースの通知システムによって PR を知ることもありますが、UI では直感的に把握しづらく、PR が見落とされやすい構造になっています。
潜在的な改善策としては、Rust のような PR 割り当てシステムを導入することが考えられます。
チャーン(Churn)
LLVM C++ API と LLVM IR は安定しておらず頻繁に変更されます。これは LLVM にとって強みでもあり弱みでもあります。
- 強み – 進化を恐れず、過去の誤りを大きなコストを伴っても修正する姿勢がある。
- 弱み – この頻繁な変更はユーザーにコストを押し付ける。特に LLVM と深く結合したフロントエンド(例: downstream バックエンド)は、API のすべての変化に追随する必要があります。
LLVM の開発哲学は「upstream か GTFO」という格言で表されます。プロジェクトは自由なライセンス下にあるため、必ずしも upstream に変更を寄与する義務はありません。しかし、上流へ貢献しない限り、そのコードは上流の意思決定に反映されません。
現在の安定性レベルが最適でない可能性はありますが、別の位置に移すと大きな外部的影響があります。LLVM の規模が非常に大きいため、大幅な変更を行うこと自体が極めて困難です。
ビルド時間(Build time)
LLVM は巨大プロジェクトであり、C++ コードは 250 万行以上、モノレポ全体では約 900 万行のコードがあります。そのためすべてをコンパイルするには相当な時間がかかります。高速ハードウェアやビルドファームにアクセスできれば耐えられますが、低スペックのラップトップでビルドしようとすると面倒です。
さらに、デバッグ情報付きでビルド(常に推奨しません)するとリンク時間が遅くなり、OOM のリスクやディスク使用量が膨大になります。共有ライブラリ化・dylib ビルドの採用、split‑dwarf の利用、lld への切替などで回避できるケースもありますが、それらには専門知識が必要です。
この分野で有望な改善策は以下の通りです:
- 事前コンパイルヘッダー(PCH)の活用(ビルド時間を大幅に短縮)。
- デフォルトで dylib ビルドへ移行(特に debuginfo ビルド時のディスク使用量とリンク時間を削減)。
- テストオーバーヘッドをデーモン化して軽減(厳密には「ビルド時間」ではないが、開発サイクルに関係)。
CI の安定性(CI stability)
LLVM の CI は 200 超のポストコミットビルドボットで構成され、多様なハードウェアプラットフォームと設定で LLVM をテストします。コミットがビルドボットを緑から赤に変えると、コミッターへメール通知が送られます。
残念ながら、この CI は常に完全に緑という状態になりませんし、不安定さも併せ持っています。主な原因はテストのフラグギリ(典型的には lldb や openmp で)とビルドボット固有の問題です。その結果、任意のコミットに対して「ビルドボット失敗通知」が当たり前になり、真に重要な失敗を見逃しやすくなるという「ノイズ」が発生します。
PR での pre‑merge テスト導入は全体的な CI 状況を改善しましたが、ビルドボット自体の問題には直接作用していません。フラグギリテスト/ビルドボットを真剣に扱う前に進展は難しいでしょう。
「ロケットサイエンスではないので bors/merge queue を使えば常に緑になる」という主張もありますが、これはスケールの問題です。典型的な作業日には 150 超のコミットがあり、均等に分布すると 10 分ごとに1件以上のコミットが生じます。多くのビルドボットは数時間かけて動き続けるため、調整は容易ではありません。
エンドツーエンドテスト(End‑to‑end testing)
LLVM は非常に網羅的なテストカバレッジを持っています。新しい最適化が正しく機能することを保証するため、ポジティブ/ネガティブ両方のテストを重視しています。しかしこれらは「単一最適化パスまたは解析に対するユニットテスト」に過ぎません。
最適化パイプライン全体(フェーズオーダリングテスト)のカバレッジは少なく、パス間の相互作用によって回帰が発生しやすいです。ミドルエンドとバックエンドを組み合わせたテストはほぼ存在せず、改善余地があります。ただしそれにはトレードオフも伴います。
私が実際に懸念しているのは「エンドツーエンド可执行ファイルテスト」です。LLVM の本来のテストスイートにはこれらがまったくありません。可执行テストは別リポジトリ
llvm-test-suite にあり、通常の日常開発では使われずビルドボットで実行されます。このリポジトリはベンチマークからユニットテストまで多岐にわたるコードを含みますが、総合的なカバレッジは不足しています。特に異なる浮動小数点フォーマットや整数サイズ、ベクトルサイズ・要素タイプの操作検証などが欠落しています。
C/C++ を介したテストには型サポートの多様性(ターゲットの psABI に定義されていない型は C コンパイラが露出しない)という制限があります。しかし Zig で同等のテストを実装すれば、全ての型を網羅できる可能性があります。
バックエンドの分岐(Backend divergence)
ミドルエンドは統一されていますが、バックエンド実装は異質です。特定バックエンドに対してのみ問題(性能・正確性)を修正する傾向があります。
例:
- ターゲット固有の DAG コンバインを汎用版ではなく導入。
- 最適化に対して多数のターゲットフックを設ける—これは他ターゲットへの影響を考慮せず、個人が手間を省くため。
理解はできます。異なるターゲットで変更を評価できない場合は進捗が遅れることがあります。しかし結果として分岐と重複が増大します。
エンドツーエンドテストの欠如もこの問題を悪化させます。そうしたテストは「すべての操作がクラッシュなくコンパイルされ、テスト対象ターゲットで正しい結果を生成する」ことを保証する強制力となります。
コンパイル時間(Compilation time)
LLVM は JIT 用ケースでも大規模 IR を生成するケースでも遅いです。私はコンパイル時間の追跡を始めてから、ターゲット別に改善しつつありますが、まだ多くの余地があります。LLVM 自体は「速くない」わけではなく、「あまり遅くない」だけです。
特に
-O0 のコンパイル時間は悪い傾向があります。LLVM は最適化を前提として設計されており、オプティマイゼーションが無効であっても多くのコストが残ります。TPDE(代替バックエンド)は、数倍以上高速に実行できることを示しています。
パフォーマンス追跡(Performance tracking)
LLVM には「公式」のパフォーマンス追跡インフラは存在しません。多くの組織が自社ワークロードで downstream の LLVM を独自に追跡します(これは実際のワークロードに焦点を当てるメリットがあります)。しかし公開されているパフォーマンスデータが不足しているため、貢献者は変更の影響を評価しづらい状況です。
LLVM には LNT インスタンスがありますが、現在は壊れており UX が悪く、データ量も少なく、PR のテスト実行要求もできません。
IR デザイン
Undef 値(Undef values)
未初期化メモリを表すために使用される値です。ある集合から任意の値を取り得ることを示します。主な問題点は:
- マルチユース問題 – undef は各利用時に異なる値になる可能性があり、利用回数を増やす変換は一般的に無効。
- 推論困難 – 人間にとって理解しづらく、証明チェッカーでは計算コストが高い。
将来的には未初期化メモリは「ポイズン値」で表現される可能性が高いですが、LLVM は現在のところポイズンを正しく扱えません。適切なサポートにはバイト型など追加 IR 機能が必要です。
不整合と仕様不備(Unsoundness and specification incompleteness)
長年知られたまま残る誤コンパイルは依然として存在します。これらは多くの場合理論的な例で、LLVM の IR 設計上の問題に直面しています。
代表例が provenance モデルです:プロヴァナンスと整数キャスト・型ポニングとの相互作用は複雑で、トレードオフが生じます。
制約エンコード(Constraint encoding)
最適化コンパイラは「この値は非負」「この加算はオーバーフローしない」などの制約を表現する必要があります。LLVM にはポイズンフラグ、メタデータ、属性、assume など多様なエンコード手段が存在します。各手段は「どれだけ情報を埋め込めるか」「最適化中に信頼性はどうか」「最適化への悪影響は?」という観点でトレードオフがあります。
浮動小数点のセマンティクス(Floating‑point semantics)
IEEE 754 の「厳密に準拠」した世界から外れると以下の問題が生じます:
- シグナル NaN と FP 例外、FP 環境の非デフォルト化。
- デノーマル値の扱い(FTZ vs. IEEE per scalar/vector)。
- x87 FPU のような高精度演算での余剰精度処理。
その他技術的課題
部分移行(Partial migrations)
LLVM は巨大なコードベースであり、大規模変更は数年かかります。代表例:
- New pass manager – 10 年前に導入、ミドルエンドではデフォルトだがバックエンドはレガシー PM を使用。
- GlobalISel – SelectionDAG の代替を意図しているが、多くのターゲットで完全移行されていない。
両者とも多くのターゲットに関わり、レガシーコードを退役させるまで時間がかかります。
ABI / コールコンベンション(ABI / calling convention handling)
コールコンベンションの扱いは散乱しています:
- 責務はフロントエンドとバックエンドに分散。
- 契約に関するドキュメントがなく、開発者は Clang の実装をコピーし(微妙な誤りが入りやすい)。
- ターゲット機能によって ABI が変わると互換性が崩れます。
ABI ライブラリ化で解決できる可能性があります。
Builtins / libcalls
真偽の源は
TargetLibraryInfo(TLI)と RuntimeLibcalls の二つです。TLI はミドルエンド、RuntimeLibcalls はバックエンドで使用されますが、後者はターゲットトリプルに依存し、利用可能な libcalls に関して「最小共通語彙」の仮定を強いる現状があります。
Rust のコンパイラビルトインなど他のランタイムライブラリ向けのカスタマイズポイントが不足しています。進行中の作業で改善されるべきです。
コンテキスト / モジュールの二項(Context / module dichotomy)
LLVM には二つの高レベルデータ保持オブジェクトがあります:
- Module – 関数・グローバルを保持し、コンパイル単位を表す。
- Context – 定数と型を保持し、複数モジュールで共有。
定数・型はデータレイアウトにアクセスできず、これは常に摩擦の原因です。明示的なリマッピングにより複雑さを軽減し、コンテキスト間リンクが可能になるかもしれません。
LICM のレジスタ圧力(LICM register pressure)
Loop Invariant Code Motion(LICM)はターゲット固有のコストモデリング無しで命令をループ外へ持ち出します。これによりライブレンジとレジスタ圧力が増し、スパイルやリロードが発生します。バックエンドはほとんどの場合、こうした命令を再びループ内に戻す(sink)ことを行いません。
締めくくり
上記は網羅的なリストではありません。さらに多くの課題がありますが、最も重要と思われるものをカバーできたと考えます。抜けている項目があればぜひ教えてください!