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


15

ここで私が意味しているのは、テンプレートからT add(T a, T b) ...生成されたコードにどのように行くかということです。これを実現するいくつかの方法を考えました。汎用関数をAST Function_Nodeに格納し、それを使用するたびに、元の関数ノードに、すべてのT型が使用されています。例えばadd<int>(5, 6)のための一般的な機能のコピーを保存するaddと、すべてのタイプに置き換えてT コピーで持ちます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ジェネリックのような意味ではありません。


C ++では(他のプログラミング言語にはジェネリックがありますが、それぞれ異なる実装方法があります)、基本的にはコンパイル時の巨大なマクロシステムです。実際のコードは、置換型を使用して生成されます。
ロバートハーヴェイ

消去を入力しないのはなぜですか?それを行うのはJavaだけではなく、悪いテクニックでもありません(要件によって異なります)。
アンドレスF.

@AndresF。私の言語がどのように機能するかを考えると、うまく機能しないと思います。
ジョンフロー

2
どのようなジェネリックについて話しているのかを明確にする必要があると思います。たとえば、C ++テンプレート、C#ジェネリック、Javaジェネリックはすべて非常に異なっています。あなたはJavaジェネリックを意味しないと言いますが、あなたが何を意味するかは言いません。
svick

2
これは本当に広すぎることを避けるために、1つの言語のシステムに焦点を当てる必要があります
Daenyth

回答:


14

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

最新のコンパイラの仕組みを知りたい場合は、最新のコンパイラのソースコードを読むことをお勧めします。まず、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が型に置き換えられた自分自身のコピーを保存します使用されています。

ジェネリックではなく、テンプレートを記述しいます。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 T あるタイプ。

  • 型間の関係を記述する規則(割り当て時の互換性、式の型の決定方法など)は、コンパイラで設計および実装されます。

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

  • 実行時に、JITコンパイラーはバイトコードをマシンコードに変換します。一般的な特殊化が行われている場合、適切なマシンコードを構築する必要があります。

たとえば、C#では

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

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


9

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

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

コンパイル、型チェック、および同じ方法での呼び出しが可能

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

ただし、ジェネリックは呼び出し側ではるかに多くの情報をタイプチェッカーに提供します。この追加情報は、特にジェネリック型が推測される場合、型変数で処理できます。型チェック中に、各ジェネリック型は変数に置き換えることができます。それを呼び出しましょう$T1

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

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

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

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


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

  1. 指定されたテンプレート引数にテンプレートを適用します。たとえば、template<class T> T add(T a, T b) { … }as 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 ++に似たテンプレートアプローチです。ただし、特殊化/インスタンス化されたテンプレートは、メインテンプレートの「バージョン」として保存します。これは誤解を招く可能性があります。概念的には同じではなく、関数の異なるインスタンス化は大きく異なる型を持つことができます。関数のオーバーロードも許可すると、長期的には事態が複雑になります。代わりに、名前を共有するすべての可能な関数とテンプレートを含むオーバーロードセットの概念が必要になります。オーバーロードを解決する場合を除き、インスタンス化されたさまざまなテンプレートは互いに完全に分離されていると考えることができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.