web-dev-qa-db-ja.com

ジェネリックは最新のコンパイラーにどのように実装されますか?

ここで私が意味することは、テンプレートT add(T a, T b) ...から生成されたコードにどのように移行するかです。これを実現するいくつかの方法を考えました。ジェネリック関数をAST as _Function_Node_として保存し、それを使用するたびに元の関数ノードに保存します。すべてのタイプTを使用した自身のコピー。使用されているタイプで置き換えられます。たとえば、add<int>(5, 6)は、addのジェネリック関数のコピーを格納し、タイプTコピー内 with int

したがって、次のようになります。

_struct Function_Node {
    std::string name; // etc.
    Type return_type;
    std::vector<std::pair<Type, std::string>> arguments;
    std::vector<Function_Node> copies;
};
_

次に、これらのコードを生成し、_Function_Node_のコピーのリストcopies.size() > 0にアクセスすると、すべてのコピーに対してvisitFunctionを呼び出します。

_visitFunction(Function_Node& node) {
    if (node.copies.size() > 0) {
        for (auto& node : nodes.copies) {
            visitFunction(node);
        }
        // it's a generic function so we don't want
        // to emit code for this.
        return;
    }
}
_

これはうまくいきますか?最近のコンパイラはこの問題にどのように取り組みますか?おそらくこれを行う別の方法は、コピーをASTに挿入して、すべてのセマンティックフェーズを実行できるようにすることです。また、即時形式で生成することもできると思います。たとえば、RustのMIRやSwifts SILなどです。

私のコードはJavaで書かれています。ここでの例はC++です。これは、例のほうが冗長ではないためですが、原則は基本的に同じです。質問ボックスに手動で書き出されただけなので、いくつかのエラーがあるかもしれませんが。

この問題に取り組むための最良の方法と同様に、私は現代のコンパイラを意味していることに注意してください。そして、私がジェネリックと言うとき、私はJavaジェネリックが型消去を使用するような)という意味ではありません。

15
Jon Flow

ジェネリックは最新のコンパイラーにどのように実装されますか?

最新のコンパイラがどのように機能するかを知りたい場合は、最新のコンパイラのソースコードを読むことをお勧めします。私は、C#およびVisual Basicコンパイラーを実装するRoslynプロジェクトから始めます。

特に、タイプシンボルを実装するC#コンパイラのコードに注目します。

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Symbols

また、変換規則のコードを確認することもできます。ジェネリック型の代数的操作に関連するものはたくさんあります。

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions

後者を読みやすくするために一生懸命に努力しました。

これを実現するいくつかの方法を考えました。ASTとしてFunction_Nodeにジェネリック関数を格納し、それを使用するたびに、元の関数ノードにそれ自体のコピーを格納しますTのすべてのタイプは、使用されているタイプで置き換えられます。

あなたはtemplatesを記述しており、genericsではありません。 C#とVisual Basicには、型システムに実際のジェネリックがあります。

簡単に言えば、彼らはこのように動作します。

  • まず、コンパイル時に型を構成する規則を確立することから始めます。例:intはタイプ、タイプパラメータTはタイプ、すべてのタイプX、配列タイプX[]も型などです。

  • ジェネリックの規則には、置換が含まれます。例えば、 class C with one type parameterはタイプではありません。タイプを作成するためのパターンです。 class C with one type parameter called T, under substitution with int for Tis型です。

  • タイプ間の関係(割り当て時の互換性、式のタイプを判別する方法など)を説明するルールは、コンパイラーで設計および実装されます。

  • メタデータシステムでジェネリック型をサポートするバイトコード言語が設計および実装されています。

  • 実行時に、JITコンパイラはバイトコードをマシンコードに変換します。これは、一般的な特殊化を前提として、適切なマシンコードを構築する責任があります。

たとえば、C#では次のように言うと

class C<T> { public void X(T t) { Console.WriteLine(t); } }
...
var c = new C<int>(); 
c.X(123);

次に、コンパイラはC<int>、引数intTの有効な置換であり、それに応じてメタデータとバイトコードを生成します。実行時に、ジッターはC<int>が初めて作成され、適切なマシンコードを動的に生成します。

14
Eric Lippert

ジェネリック(またはパラメトリックポリモーフィズム)のほとんどの実装は、型消去を使用します。これにより、汎用コードのコンパイルの問題が大幅に簡略化されますが、ボックス化された型でのみ機能します。各引数は事実上不透明なポインターであるため、引数に対して操作を実行するには、VTableまたは同様のディスパッチメカニズムが必要です。 Javaの場合:

_<T extends Addable> T add(T a, T b) { … }
_

コンパイル、型チェック、および以下と同じ方法で呼び出すことができます。

_Addable add(Addable a, Addable b) { … }
_

exceptジェネリックは、呼び出しサイトで型チェッカーにはるかに多くの情報を提供します。この追加情報はtype variablesを使用して処理できます(特にジェネリック型が推論される場合)。型チェック中に、各ジェネリック型を変数に置き換えることができます。これを_$T1_と呼びましょう。

_$T1 add($T1 a, $T1 b)
_

型変数は、具体的な型に置き換えることができるまで、既知になるとより多くのファクトで更新されます。型チェックアルゴリズムは、完全な型にまだ解決されていない場合でも、これらの型変数に対応できるように記述する必要があります。 Java自体では、関数呼び出しのタイプを知る必要がある前に引数のタイプがわかっていることが多いため、これは通常簡単に実行できます。注目すべき例外は、関数引数としてのラムダ式です。このような型変数を使用する必要があります。

ずっと後で、オプティマイザmayは、特定の引数のセットに対して特別なコードを生成します。これは、事実上、一種のインライン化になります。

ジェネリック型の引数のVTableは、ジェネリック関数が型に対して操作を実行せず、それらを別の関数に渡す場合にのみ回避できます。例えば。 Haskell関数call :: (a -> b) -> a -> b; call f x = f xは、x引数をボックス化する必要はありません。ただし、これには、サイズを知らなくても値を渡すことができる呼び出し規約が必要です。これにより、値が本質的にポインタに制限されます。


C++は、この点でほとんどの言語とは大きく異なります。テンプレート化されたクラスまたは関数(ここではテンプレート化された関数についてのみ説明します)は、それ自体を呼び出すことはできません。代わりに、テンプレートは、実際の関数を返すコンパイル時のメタ関数として理解する必要があります。テンプレート引数の推論を少しの間無視すると、一般的なアプローチは次のステップに要約されます。

  1. 提供されたテンプレート引数にテンプレートを適用します。たとえば、template<class T> T add(T a, T b) { … }add<int>(1, 2)として呼び出すと、実際の関数int __add__T_int(int a, int b)(または、名前をマングルする方法が使用されます)が得られます。

  2. その関数のコードが現在のコンパイル単位ですでに生成されている場合は、続行します。それ以外の場合は、関数int __add__T_int(int a, int b) { … }がソースコードに記述されているかのようにコードを生成します。これには、テンプレート引数のすべての出現箇所をその値で置き換えることが含まれます。これはおそらくAST→AST変換です。次に、生成されたASTで型チェックを実行します。

  3. ソースコードが__add__T_int(1, 2)であるかのように呼び出しをコンパイルします。

C++テンプレートには、オーバーロード解決メカニズムとの複雑な相互作用があることに注意してください。ここでは説明しません。また、このコード生成により、テンプレート化されたメソッドも仮想化することが不可能になることに注意してください。型消去ベースのアプローチは、この実質的な制限の影響を受けません。


これはコンパイラや言語にとって何を意味しますか?あなたが提供したいジェネリックの種類について慎重に考えなければなりません。ボックス化された型をサポートする場合、型推論がない場合の型消去は、可能な限り最も簡単なアプローチです。テンプレートの特殊化はかなり単純に見えますが、テンプレートは定義サイトではなく呼び出しサイトでインスタンス化されるため、通常は名前のマングルと(複数のコンパイルユニットの場合)出力の大幅な重複が含まれます。

あなたが示したアプローチは、本質的にC++に似たテンプレートアプローチです。ただし、特殊化/インスタンス化されたテンプレートは、メインテンプレートの「バージョン」として保存します。これは誤解を招く可能性があります。それらは概念的に同じではなく、関数の異なるインスタンス化は大きく異なるタイプを持つ可能性があります。関数のオーバーロードも許可すると、長期的にはこれが複雑になります。代わりに、名前を共有するすべての可能な関数とテンプレートを含むオーバーロードセットの概念が必要になります。オーバーロードの解決を除いて、インスタンス化されたさまざまなテンプレートは互いに完全に分離していると考えることができます。

9
amon