web-dev-qa-db-ja.com

動的に型付けされた言語のネイティブマシンコードへの事前コンパイル

それは私の理解です:

  1. 事前にコンパイルすることは特に難しい、効率的なネイティブマシンコード動的に型付けされたPythonのような言語。
  2. 主に上記の結果として、これらの言語の実装はしばしば interpreted (通常はJITCで)。言い換えると、主にポイント1の結果として、これらの言語の実装はしない AoT machine コードにコンパイルし、代わりに AoT bytecode にコンパイル次に、できる限りベストを尽くします(実行時のJITC /キャッシュなど)。

上記は正しいですか?もしそうなら、なぜ? (例:Python)。 複雑さの理論、より具体的には停止の問題自体(?)は、この課題について nameバインディング/ディスパッチ 。しかし、これが特にこれらの言語のいずれか、またはより大きなクラスの言語のいずれかについて、正式に調査されたであるかどうかを知りたい。

おそらく上記にも役立つでしょうが、ネイティブマシンコードへの「実用的」(効率的)AoTコンパイラを使用した動的型付け言語の注目すべき例は何ですか?


関連する質問

実際、pythonをネイティブマシンコードにコンパイルするのは簡単です。実際、Cythonコンパイラはそれを実行できます。

ただし、Cythonコンパイラーは次のようなコードを変換します。

A = B + C

次のCコードのようなものに:

PyObject * A = PyNumber_Add(B, C);

PyNumber_Addは、BとCのタイプのaddの実際の実装を呼び出すために最終的に関数ポインターを呼び出す汎用関数です。これは、ご想像のとおり、単純に数値を直接追加するコードよりもはるかに低速です。

したがって、問題は動的型付け言語のマシンコードを生成できるかどうかではなく、簡単です。問題は、効率的なマシンコードを生成できるかどうかです。効率的なコードを生成するには、変数のタイプを把握して、任意のタイプで機能する汎用コードの代わりに、そのタイプに固有の高速コードを生成できるようにする必要があります。

停止の問題は、タイプを常に把握できるとは限らないことを示しています。次の例を検討してください。

def crazy():
   if really_hard_math_problem():
      return 42
   else:
      return "hello!"

この関数のタイプを正しく判別するために、コンパイラーは本当に難しい数学の問題の解決策を理解する必要があります。つまり、常に正しい答えを得るには、コンパイラーはすべての可能な数学の問題を解決できなければなりません。

しかし、これには間抜けなことがある。あなたはそうしないか、少なくともこのようなコードを書くべきではありません。現実的なプログラムには、より健全なタイプがあり、未解決または解決不可能な問題に依存しません。したがって、おそらく、現実的なプログラムですべてのタイプを解決することが可能でしょう。ここでは、停止の問題は実際的な問題ではありません。

Julia言語は動的型付け言語であり、広範な型推論を使用して効率的なコードを生成します。コードをトレースすることにより、ほとんどの変数のタイプを解決できます。

5
Winston Ewert

ソースコードのチャンクを取得し、それらを(直接または間接的に)マシンコードに変換して実行します。すべてを1つのパスで実行する場合は、事前に実行されます。実行とインターリーブされたチャンクでそれを行う場合、それはちょうど間に合います。

通訳とは何ですか?私の推測では、コンパイルしたものを再利用してより大きなチャンクに変換するのではなく、翻訳済みのチャンクを忘れてしまった場合を除いて、時間だけをコンパイルすることになります。


Pythonのような動的に型付けされる言語であるネイティブマシンコードに事前にコンパイルすることは特に困難です。

はい。不可能ではない。

主に上記の結果として、これらの言語の実装はしばしば解釈されます。

はい。まあ、あなたが解釈するものに依存します。しかし、ちょうど間に合いました、はい。


どうして?

分解してみましょう...

ご存知のとおり、ターゲットマシンを知る必要があります。つまり、デプロイ前に複数のターゲットマシンのコードをコンパイルするか、デプロイ時にそれを実行します(インストーラーが事前にコンパイルします)。


では、型消去についてお話ししましょう。 Javaブランドではありませんが、私はそこには行きません。)ではなく、真のタイプの消去。

コンパイラーがすべてのタイプを事前に知っている場合、それは静的分析中です。これは静的タイプを使用すると簡単です...

次に、各ルーチン/メソッド/関数がどのように呼び出されるかを正確に把握しているため、正確なメモリレイアウトとその操作を実行する方法を把握しています。したがって、そのために最適化されたネイティブコードを生成でき、実行時に型チェックを行う必要はありません。

そのおかげで、コンパイラは型情報を必要としないネイティブコードを記述できます。つまり、ランタイムから型情報を消去できます。したがって、型消去。


行き先はわかりますが、途中に行きたい駅がいくつかあります。最初のストップ:ジェネリック。

ジェネリック型がある場合、事前にわかっているかどうかわからない型パラメーターがあります。アプリケーションの場合、コンパイラーは、型が使用されているすべての場所を調べ、各バリアントに対して最適化されたコードを発行できます。これはまた、事前にオープンジェネリック型を使用してライブラリをコンパイルすることは、最終的なアプリケーションでのみ可能であることを意味します。

ちなみに、ライブラリを任意の形や形式で使用するには、何らかの形でタイプ情報が必要です。したがって、完全な型の消去ではありません。

2番目のストップ:反射と放出。

実行時に新しいタイプを作成したいとしましょう。まあ、そのタイプの情報はランタイムでのみ利用可能です特に外部入力に依存する場合

さらに、モンキーパッチ(実行時に型が変更される可能性がある)や「評価」のようなものがある場合、それはさらに悪いことです。


宛先:動的タイプ。

変数とパラメーターのタイプがわからない場合、最適化されたコードを出力できません。動的に型付けされた言語で静的分析を行い、型情報を取得することが可能です。ソースは型をマークしないため、これは大きなタスクです...静的分析はコードの値に従う必要があります...そしてあなたは何を知っていますか?コードを実行して値がどこに行くかを確認する方が簡単です(これはコンパイルのタイミングです)。コンパイラーは、可能なすべてのブランチをたどることを除いて、本質的にそれをしなければなりません。

ジャストインタイムのコンパイルでは、オンデマンドで実行する場合を除いて、ネイティブマシンコードが引き続き生成されます。

ルーチン/メソッド/関数が呼び出されると、JITは、それが取る型の特定の組み合わせに対してすでにコンパイルされているかどうかを確認します。コンパイルされている場合は、そのマシンコードを実行できます。それ以外の場合は、型がわかったので、コンパイルして実行できます。 最初にネイティブコードをすばやく(起動時間を短縮して)放出し、その後、バックグラウンドでより最適化されたコードを置き換えて置き換える階層型JITシステムもあります。


さて、ひねりがあります。次善のコードを受け入れるとどうなるでしょうか。上記では、最適化されたコードと言っていました。ただし、最適ではないコードを使用して、マシンコードの動的型を処理できます。特に、プリミティブ型にはバリアント型を使用でき、複合型にはEntity-Component-Systemのようなものを使用できます。

動的に型付けされた言語からの完全なネイティブコードになります。ただし、JITよりもパフォーマンスが低下する可能性があります。

いいえ、これはジェネリックを解決しません。動的タイプを解決します。


したがって、動的に型付けされた言語は、事前にネイティブコードにコンパイルすることが困難です。しかし、それらはしばしばジャストインタイムでコンパイルされます。

また、可能な限り事前にコンパイルし、他のすべてにJITを使用するソリューションがあることにも触れたいと思います。


私は複雑さの理論(より具体的には、問題名のバインディング/ディスパッチに関しては停止する問題自体)が実際にここで非難されていると思います

タイプ情報を理解しようとすることには類推があり、それが時々決定できないと思います。

しかし、それは実際には停止の問題ではありません。ほとんどのコンパイラーは、決して停止しないマシンコードを生成します(お好きな言語で無限ループを試してください)。彼らは気にしない。彼らはプログラムが停止することを証明しようとはしません。

証明の自動化を行う言語があり、プログラムが停止するかどうかをチェックし、次の3つのうちの1つを提供します。a)停止することがわかっている。 b)よくわかりません。自己責任で進めてください。 c)停止しないことがわかっている。これらの言語は動的に型付けされません。


しかし、特にこれらの言語のいずれか、またはより大きなクラスの言語のいずれかについて、これが正式に研究されたかどうかを知りたいのですが。

スキップ


おそらく上記にも役立つでしょうが、ネイティブマシンコードへのAoTコンパイラーを使用した動的型付け言語の注目すべき例は何ですか?

あなたはPythonを要求するので、私は見てみました。いくつかのオプションがあるようです:Nukita、mypyc、Numba。 私はそれらの多くを知りません。

.NETまたはJavaで実行できる言語で、事前にコンパイルソリューションがすでにある場合は、事前にコンパイルできます。それらはたくさんの言語です。そして、いくつかの事前コンパイルがすでにあります。最初に頭に浮かぶのはRubyです。 はい、「評価」とモンキーパッチが含まれています。プログラムがユーザー入力に依存する場合があるため、すべてのプログラムを事前にコンパイルすることはできません。

それが事です。動的に型付けされた言語のすべての有効なプログラムを事前にコンパイルできるわけではありません。プログラムが実行時に型を作成または変更する場合、その型情報を事前に取得する方法はありません。同じことは、オープンジェネリック型を持つライブラリにも当てはまります。 言語の複雑さや停止の問題が原因ではなく、時空の連続性が原因です。まだ存在していない型の型情報を取得するためのタイムトラベルを把握していません。

それでも、上で言ったように、次善のコードになる可能性があります。 .NET Dynamic Language Runtimeを見たことはありますか? .NETはリフレクションを使用して静的に型付けされています。 .NET動的言語ランタイム(完全な.NETコード)を使用して、動的に静的に型指定されたオブジェクトを持つことができます。これは、PythonおよびRubyから.NETに移植されたために開発されたもので、他の言語が続いていました(これらの移植はメインの背後にあることに注意してください)これらの言語のバージョン)。

ちなみに、これを行うことができます。ネイティブ型、関数、オブジェクトのみになるようにします。新しいタイプは作成できません。継承も何もありません。代わりに、実行時にオブジェクトにプロパティをアタッチできます。メソッドはオブジェクトにアタッチされた関数であり、複数の類似のオブジェクトを作成するためのクローンを作成できます。辞書、マップなどとしてランタイムに実装します。ちょっと待ってください、それはJavaScriptのように聞こえます…

JavaScriptプログラムの事前コンパイル 。うん。 前もって、もちろん、デプロイ時。

ええ、事前コンパイルの本当の問題は動的型の部分ではありません。問題は、コードの発行、オープンなジェネリック、および類似のメタプログラミング形式です。

3
Theraot

Pythonのように、事前にコンパイルする、効率的なネイティブマシンコード動的に型付けされる言語をコンパイルすることは特に困難です。

これはほとんど当てはまりますが、「効率的な」部分が原因であり、現在の主流のCPUおよびOSの設計方法が原因です。

Pythonを事前にコンパイルすることはnot困難です。実際、CPythonの実装は、あなたが気付くことなく、それを行います。 「ネイティブ」のマシンコードにコンパイルしないだけです。 (技術的には、CPython仮想マシンのisネイティブマシンコードです。結局のところ、「ネイティブマシンコード」について話すときは、常にwhatあなたが話しているマシン!)

また、Pythonをネイティブマシンコードに事前にコンパイルすることも難しくありません。結局のところ、通訳は何をしているのでしょうか?抽象構文ツリーをたどり、そこにあるすべての式について、その式を実装する少しのコードを実行します。理論的には、代わりにその小さなコードをファイルに書き込むことができます。非リーフノードの場合、本質的には構成要素の式の可能なすべてのタイプのコードを生成する必要があるため、これは恐ろしく非効率的なコードになります(インタープリターとは異なり)、それらがまだ何であるかがわからないためです。

他の回答で述べたように、いつでもフォールバックして、インタプリタが呼び出すのと同じランタイム関数を呼び出すだけのネイティブマシンコードを生成できます。あなたはそれがごまかしであると言うかもしれませんが、Cでさえもランタイムライブラリなしでは機能しません。 (自立したCを書こうとしたことがあれば、それがどれほど難しいかご存じでしょう。)

効率的なネイティブマシンコードにPythonを事前にコンパイルすることも難しくありません。[~#~]しかし[〜#〜]Pythonを効率的に実行するために設計されたマシンを使用する必要があります!最新の主流のOSとCPUは、Cのような言語を効率的に実行するように設計されています。これらには、Cに明示的に存在する仮想メモリ管理などの機能が含まれており、Pythonを遅くします。 (特に、仮想メモリはガベージコレクションのパフォーマンスを低下させます。)

現在、これはすべてnotが不可能であることを意味します!たとえば、Common LISP(SBCL、CMUCL、ClozureCL)およびScheme(Ikarus、Gambit、Chicken、Chez)の高性能AOTコンパイル済み実装があります。

また、Objective-Cは動的言語であり、効率的なネイティブマシンコードにコンパイルできることに驚かれる人はいません。

  1. 主に結果として、上記のこれらの言語の/-実装は、しばしば解釈されます

しかし、それは真実ではありません。

動的言語の高性能実装は、通常、areコンパイルされますが、事前にコンパイルされるわけではありません。just-in-timeコンパイルされています。

これにより、コンパイラーはすべてのランタイム情報にアクセスでき、さらには、事前のコンパイラーが決して持っていない統計情報にもアクセスできます。停止問題やライスの定理などの問題は、staticallyプログラムを実行せずに分析する場合にのみ適用され、notが適用されます実行中のプログラムが何をしているかを単に監視するため。

私のこの回答では、さまざまな実行モデルとコンパイルモデルについて書きました。

Stack Overflowには2つの質問があります。

3
Jörg W Mittag