ブログ 約6分

よく設計されたプロジェクトの利点:GCC vs Clang

Share this article
よく設計されたプロジェクトの利点:GCC vs Clang

GCC(GNU Compiler Collection)とClangは、ソフトウェア開発の世界で最も著名な2つのC++コンパイラです。それぞれが異なるニーズや好みに対応する独自の設計哲学とアーキテクチャを持っています。この記事では、GCCとClangの根本的な設計の違いを探り、これらの違いが機能性、パフォーマンス、使いやすさにどう影響するかを強調します。

アーキテクチャ設計の違い

  • Clang:Clangの設計は高度にモジュラーです。独立して、または組み合わせて使用できる、明確に定義された一連のライブラリ(フロントエンド、ミドルエンド、バックエンド)で構成されています。このモジュール性により、Clangは容易に拡張可能で保守しやすくなっています。LibClangはClangライブラリへの安定したCインターフェースも提供し、ツールやIDE統合の開発を容易にしています。
  • GCC:歴史的に、GCCは密結合したコンポーネントを持つモノリシックなコンパイラとして設計されました。時間とともによりモジュラー化されてきましたが、Clangほどモジュラーではありません。GCCはプラグインをサポートしていますが、そのアーキテクチャはClangのより柔軟な設計と比較して拡張が困難です。

Clangの設計を詳しく見て、そのアーキテクチャがなぜ新機能やプラグインの追加を容易にするのかを理解しましょう。

Clangの設計

他の多くのコンパイラ設計と同様に、Clangコンパイラには3つのフェーズがあります:

  • フロントエンドはソースコードを解析し、エラーをチェックし、入力コードを表現する言語固有の抽象構文木(AST)を構築します。
  • オプティマイザは、フロントエンドが生成したASTに対して最適化を実行します。
  • バックエンドは、ターゲットアーキテクチャ向けの最終的なマシンコードを生成します。

Clangを他のコンパイラと異なるものにしているのは何か?

その設計における最も重要な違いは、ClangがLLVMをベースにしていることです。LLVMの背後にあるアイデアは、LLVM中間表現(IR)を使用することです。これはJavaのバイトコードのようなものです。
LLVM IRは、コンパイラのオプティマイザセクションにある中間レベルの解析と変換をホストするように設計されています。軽量な実行時最適化、関数間/手続き間最適化、プログラム全体の解析、積極的な再構造化変換などのサポートを含む、多くの具体的な目標を念頭に設計されました。しかし、その最も重要な側面は、明確に定義されたセマンティクスを持つ第一級の言語として定義されていることです。

この設計により、コンパイラの大部分を再利用して他のコンパイラを作成できます。例えば、フロントエンドだけを変更して他の言語を処理できます。

I - フロントエンド

Clangはモジュラーに設計されており、各コンパイルフェーズは特定のモジュールによって実行されます。フロントエンドフェーズに関わるプロジェクトの一部は次のとおりです:

あらゆるフロントエンドパーサーと同様に、レクサーと意味解析が必要です。Clangフロントエンドは-cc1引数を渡すことで実行できます。AST生成など、いくつかの機能を提供します:

clang -cc1 -ast-dump test.c

このコマンドはcc1_main関数によって処理されます。実行される興味深いメソッドのいくつかのシーケンスは次のとおりです:

clang11

メソッド ExecuteActionFrontEndAction型のパラメータを持ちます。目的は、どのフロントエンドアクションを実行するかを指定することです。FrontEndActionは抽象クラスで、具体的なフロントエンドアクションを実装するにはそれを継承する必要があります。

Clangが実装するすべてのフロントエンドアクションを CQLinqを使って調べてみましょう。そのためには、それを直接または間接的に継承するすべてのクラスを検索します。

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.FrontendAction”)
where depth0  >= 0 orderby depth0
select new { t, depth0 }

多くのフロントエンドアクションが利用可能です。例えば、ASTDumpActionは最終的な実行ファイルを作成せずにASTを生成します。ほぼすべてのフロントエンドアクションはASTFrontEndActionを継承しており、生成されたASTで動作することを意味します。

この設計の興味深い点は、カスタムのFrontEndActionを簡単にプラグインできることです。新しいものを実装するだけです。

ASTに対してどうやって処理を実行できるか?

各ASTFrontEndActionは1つ以上のASTConsumerインスタンスを作成します。ASTConsumerクラスは抽象クラスで、特定のニーズに合わせて独自のASTコンシューマを実装する必要があります。

FrontEndActionは、次の依存関係グラフで指定されているようにASTコンシューマを呼び出します。

すべてのASTConsumerクラスを CQLinqほど適切なものはありません:

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.ASTConsumer”)
where depth0  == 1
select new { t, depth0 }

で検索してみましょう。CodeGeneratorはASTコンシューマの一例です。前述のとおり、LLVMの強みの1つはIRの使用であり、それを生成するにはASTの解析が必要です。CodeGeneratorはASTConsumerを継承し、IRの生成を担当するクラスで、興味深いのはこの処理がClangCodeGenという別のプロジェクトに分離されていることです。

LLVM IR生成に関わるクラスの一部は次のとおりです:

II - オプティマイザ

このフェーズを説明するには、LLVMの父であるChris Lattnerのこの 記事ほど適切なものはありません:

「最適化がどう機能するかの直感を得るには、いくつかの例を見ていくのが有用です。コンパイラの最適化にはさまざまな種類があるため、任意の問題を解決する方法のレシピを提供するのは困難です。とはいえ、ほとんどの最適化はシンプルな3部構成に従います:

  • 変換すべきパターンを探す。
  • マッチしたインスタンスに対して変換が安全/正確であることを検証する。
  • コードを更新して変換を実行する。

オプティマイザはLLVM IRを読み込み、少し加工してから、できればより速く実行されるLLVM IRを出力します。LLVMでは(他の多くのコンパイラと同様に)、オプティマイザは個別の最適化パスのパイプラインとして構成され、それぞれが入力に対して実行され、何かを行う機会を持ちます。一般的なパスの例は、インライナー(関数の本体を呼び出しサイトに代入)、式の再関連付け、ループ不変コード移動などです。最適化レベルに応じて異なるパスが実行されます。例えば-O0(最適化なし)ではClangコンパイラはパスを実行せず、-O3ではオプティマイザで67個のパスのシリーズを実行します(LLVM 2.8時点)。

「pass」クラスを継承するクラスを検索して、LLVMCoreのパスを調べてみましょう。

from t in Types
let depth0 = t.DepthOfDeriveFrom(“llvm.Pass”)
where t.ParentProject.Name==”LLVMCore” && depth0  >= 0 orderby depth0
select new { t, depth0 }

もちろん、他のLLVMモジュールにも多くのパスが存在します。

III - バックエンド

他のフェーズと同様に、バックエンドは特定のターゲット向けの出力を生成する責任があります。Clangの場合、このフェーズは高度にモジュラーです。例えば、x86ターゲット向けのコードを生成するLLVMX86Targetを見てみましょう。

x86ターゲット向けのバイナリ生成に関わるすべてのモジュールを示すグラフは次のとおりです。

このフェーズには多くのモジュールが関わり、それぞれが特定の責任を持っています。これは凝集度、クリーンなAPI、関心事の分離を促進し、開発者が全体像の小さな部分に集中できるため、システムを理解しやすくしています。

結論

LLVM/Clangのデュオは単なるC/C++コンパイラではなく、ツールを構築するためのインフラストラクチャでもあり、その動作を容易に拡張できます。多くのツールがLLVM/Clangのソースコードに標準で含まれており、他にも多くがWeb上で見つかります。

ツールを構築するためのC/C++パーサーが必要なら、Clangは非常に良い候補です。

Share this article