web-dev-qa-db-ja.com

インタープリターがコードを実行するとき:最低レベルまでの「一連の解釈」はありますか?

いくつかの調査を行い、このサイトの人々の助けを借りて、私はようやく実際に解釈が何であるかを理解するようになりました。

基本的に、解釈の意味は次のとおりです(間違っている場合は修正してください)。

  1. コードをスキャンします。
  2. 同じ意味を持つ別の言語のコードを考え出す。
  3. 実行新しいコード。

たとえば、Javaで記述されたインタプリタはCのコードを解釈し、次のコード行を確認します:printf("hello world");

次のコード行が表示されます:System.out.println("hello world");

そしてexecute it(コンパイラとは対照的にstore itになります)。

これがすべて当てはまる場合、ここに私の質問があります:

インタプリタが実行するたびに、実際には最低レベルまで一連の解釈が作成されますこれが本当かどうか知りたいのですが。これが私の意味です:

インタプリタexecutesコード行(これは、行をスキャンした後に作成された新しい行です)の場合、実際には、基盤となるプラットフォームに解釈するように指示します。

たとえば、知っているように、Javaで記述されたCコードを解釈するインタープリターは、コードの新しい行に反応してprintf("hello")およびexecutesを参照しますSystem.out.println("hello")

しかし、「executesSystem.out.println("hello")」は実際にはどういう意味ですか? これは、基礎となるプラットフォーム(この場合はJVM)にこの行を解釈するように指示することを意味します。

したがって、別の解釈が始まります。JVMの(この例では)インタプリタは、行System.out.println()を受け取り、別の言語で同じ意味を持つ新しいコード行を考え出し、そして実行するそれ。

そして、それがexecutesコードの新しい行である場合も、実際には、それを解釈するようにその基盤となるプラットフォーム(この例ではOS)に実際に伝えます。次に、OSで別の解釈が始まります。これにより、異なる言語で同じ意味を持つ新しいコード行が作成され、さらに低いレベルで解釈するように指示されます。これは、CPUの最低レベルまで続きます。

私の質問は、これは本当ですか? インタープリターが実行するすべての実行、実際には、解釈するための基盤となるプラットフォームへの呼び出しですか?(したがって、最低レベルまで一連の解釈を作成しますか?)

人々がインタープリターexecutesコードだと言うとき、彼らは実際にそれがそのコードを解釈するための基盤となるプラットフォームであることを伝えることを意味しますか? (等々?)

(注:私は特にJVMの機能について正確ではないかもしれません、そこでいくつかのステップを逃しました。しかし私の質問はより一般的です、Javaは例)。

6
NPElover

はい、それ以上です。 インタプリタのコードのすべては、「コードの一部をスキャンする」ステップを含む、より低いレベルで解釈されます。

ただし、レベルはあなたが思っているよりも少なく、3レベルを超えることはまれです。 Jythonは、インタープリターで実行されるインタープリターの一例です。これはPythonコードを実行し、それ自体がJavaで記述されています。これは、JVMによって解釈されます。これは、CPUによって解釈されるマシンコード(CまたはC++からコンパイルされたもの)です。

JVMは通常、CまたはC++で作成され、OSではなくCPUで直接実行されます。ネイティブプログラムは、オペレーティングシステムによって解釈されるのではなく、オペレーティングシステムとともにCPU上で直接実行されます。 OSとネイティブプログラムの違いは、OSがハードウェアへのアクセスを増やすことです。そのため、ネイティブプログラムがハードウェアにアクセスしたい場合(画面に表示するなど)、OSに問い合わせる必要があります。

また、「新しい言語のコード行を思い付く」とは Just-In-Time Compilation であり、厳密な解釈ではありません。伝統的にインタープリター(ほとんどすべてのJVMを含む)と考えられている多くのプログラムは、なんらかの形式のJITコンパイルを使用します。 JITコンパイラは次のようになります。

void execute(instruction instructions_to_execute[])
{
    lower_level_function f;
    if(is_translation_in_cache(instructions_to_execute))
        f = get_translation_from_cache(instructions_to_execute);
    else
    {
        f = translate_to_lower_level_instructions(instructions_to_execute);
        put_translation_in_cache(instructions_to_execute, f);
    }
    f();
}

インタープリターは次のようになります。

void execute(instruction instructions_to_execute[])
{
    for(instruction i in instructions_to_execute)
    {
        switch(i.type)
        {
        case PRINT_STRING:
            print(i.string_to_print);
            break;
        case ADD:
            variables[i.result_variable] = variables[i.left_variable] + variables[i.right_variable];
            break;
        case CALL:
            execute(functions[i.function_to_call].instructions);
            break;
        // etc
        }
    }
}

JITコンパイルは通常、コードの頻繁に実行されるセクションの方が高速です。これは、最も低いレベルのコードに変換して直接実行できるためです。コンパイラ(translate_to_lower_level_instructions関数)は遅いです。

12
user253751

基本的に、解釈の意味は次のとおりです(間違っている場合は修正してください)。

  • コードをスキャンします。
  • 同じ意味を持つ別の言語のコードを考え出す。
  • その新しいコードを実行します。

解釈された実行は最近非常にあいまいな用語ですが、解釈の意味を理解するために、これは良い見方ではありません。

基本的なレベルでは、コードの断片には、一連の実際の効果として表現できる意味があります。最下位レベルでは、機械語命令は一連のバイトであり、たとえば、「レジスターAとBの内容を数値として扱い、それらを加算して、結果をレジスターCに格納する」ことを意味します。コンピューター内の具体的な電気信号。

コードを実行するということは、実際の効果を実行することを意味します。ここでも、最下位レベル(マシンコード)では、これを単に「実行」と呼びます。 CPUのハードウェア回路は、命令の値によってトリガーされて信号をリダイレクトし、効果が発生するようにします(たとえば、加算回路の入力をレジスタAとBに接続し、出力をレジスタCに接続します-これはもちろん、非常に簡略化されたビューです物)。

ただし、コードがハードウェアで直接理解されない形式(つまり、マシンコードを除くすべて、アセンブリのような単純なものでも)の場合は、別の方法で実行する必要があります。そして、そこが解釈とコンパイルの根本的な違いです。

コードでできることの1つは、別の形式にtranslateすることです。翻訳とは、コードを一連の小さなビットとして解釈し、(テーブル、スイッチ、一連のifsなどの)同等のコードを他の言語で検索し、結果を一緒に文字列化することを意味します。

古典的には、ターゲット言語がマシンコードの場合、これはコンパイルと呼ばれます。

あなたができる他のことは、少しのコードを取り、その意味を調べ(再び、テーブル、スイッチなど)、そして既存の少しのコード(プログラムの別の小さな関数)を実行するプログラムを書くことです。意図された実際の効果。これは解釈の最も純粋な形式です。

ただし、この方法での純粋な解釈は効率的ではありません。これは混乱するところです。

純粋なインタープリターは、コードを何度も繰り返し読み取る必要があります。メモリに非常に敏感な環境で作業しない限り、これは愚かです。 (そして、あなたがそうしたとしても、それは愚かですが、別の方法でです。)より良いことは、仕事の少なくとも一部を一度だけ行うことです。高水準言語は理解が複雑であり、コードを読み取るためのI/Oはコストがかかります。だから、あなたができることがある。

たとえば、コードをより大きなチャンクで読み取り、メモリ内にASTなどの表現を構築できます。それはあなたが何度もそれを読むことからあなたを救います。ただし、ASTは元のコードの正確な構造に関する情報が多すぎるため、メモリ効率はそれほど高くありません。

もう1つの方法は、まずコードをよりコンパクトな形式に変換します。この形式は、一連の命令であり、単純です。次に、この単純なフォームの純粋なインタープリターは、単純であるため、はるかに高速に実行できます。たとえば、CPythonインタープリターがこれを行います。 (.pycファイルは、その単純なフォームのキャッシュバージョンです。)

完全なコンパイラーのルートをたどり、マシンコードに変換してCPUに実行させることもできます。一度実行してコンパイルされたコードを配布するのではなく、プログラムの起動時に実行します。これはJIT(ジャストインタイム)コンパイルと呼ばれます。

これに複数のステージを追加し、最初にいくつかの単純な形式に変換してから、それをコンパイルしてマシンコードを作成できます。そして、各ステージを実行するときにあなたは変化することができます。プログラムが配布される前。プログラムの起動時。実際の実行が関数に到達したとき。等。

したがって、可能な実行戦略の広い範囲が得られ、「コンパイル」と「解釈」という単純な用語が不鮮明になります。また、単一の言語は異なる戦略を使用して異なる実装を持つ可能性が高いため、コンパイルおよび解釈された言語はもうありません。

しかし、ここにいくつかの例があります。

CおよびC++は通常、事前にマシンコードにコンパイルされており、コンパイルされたバージョンが配布されます。もちろん、コンパイラーは非常に複雑で、中間形式も使用します。

JavaとC#は、コンパイルとも呼ばれるプロセスで事前に中間形式(それぞれJava BytecodeとMSIL)に変換され(バイトコードは構造と表現がマシンコードに似ているため)、コンパイルされた形式は配布されます。中間形式の実行はプラットフォームによって異なります。 Java実装では、最初に純粋なインタープリターが実行される傾向がありますが、最終的には、コードの頻繁に使用される部分をマシンコードにコンパイルします。Netは、私の知る限り、マシンコードにのみコンパイルします。関数の粒度で(つまり、実行が最初に関数を呼び出すとき、ILはコンパイルされ、後続の実行のために保存されます)。

標準のpython実装であるCPythonは、ファイルが最初に読み取られたときにバイトコードに変換され(結果を別のファイルにキャッシュする)、次にそのバイトコードの純粋な解釈を行います。

JavaScriptエンジンは、パフォーマンスに対する要求が非常に高いため、非常に複雑な実装戦略を持っています。たとえば、Mozillaのエンジンは、最初に1つの中間形式に変換し、それを解釈します。次に、その形式から頻繁に使用されるコードのマシンコードへのJITコンパイラーが含まれます。そして、非常に頻繁に使用されるコード(および特別に注釈が付けられたコード)では、中間形式を別の中間形式に変換し、2番目のJITコンパイラーを使用してそれをマシンコードに変換します。 (2つのJITの違いは、コンパイルに費やした時間と生成されたコードの品質(読み取り:速度)の間のトレードオフが異なることです。また、2番目の中間形式は、最適化に時間を費やすJITに適しているために存在します。 )

高級言語用の純粋なインタプリタはほとんどありません。現代の使用法では、インタープリターは、途中の翻訳の数に関係なく、高レベルのコードを取り込み、実際の効果を生み出すプログラムです。コンパイラーは、コードを取り込んでマシンコードまたは人間が読めない中間形式を生成するプログラムです。そして、VMは、このような中間的な形式を取り、実際の効果を生み出すプログラムです。したがって、実装の3つのカテゴリー(および一般的な実装で行く場合は言語)を大まかに見つけることができます。

  • 機械語を生成するコンパイラーがあるコンパイラー言語。
  • VMベースの言語。中間コードを生成するコンパイラと、それを実行するVMがあります。
  • 通訳者がいる通訳言語。
3
Sebastian Redl

基本的に、解釈の意味は次のとおりです(間違っている場合は修正してください)。

  • コードをスキャンします。
  • 同じ意味を持つ別の言語のコードを考え出す。
  • その新しいコードを実行します。

再び。これは誤った定義です。

一部の通訳はこのように動作します。しかし、そうでないものもあります。たとえば、典型的なJavaバイトコードインタープリター(例:Java -int)は、バイトコードを解釈する前に、バイトコードの変換を行う必要がない必要はありません。 (そして、マイナーな書き換えを行う場合、それはインタープリターをより速く実行するための最適化にすぎません。)

インタープリターがコード行を実行すると(これは、行をスキャンした後に作成された新しい行です)、実際に基盤となるプラットフォームに解釈するように指示します。

いいえ、不正解です。実際、インタープリターが実行されると、インタープリターは、解釈しているコードの「実行エンジン」になります。解釈されたコードを実行するのは実行エンジン自体であり、その時点で表現されている言語、中間コード、またはバイトコードで実行されます。

基盤となるプラットフォーム(JVMのバイトコードインタープリターの場合はハードウェアなど)がインタープリター自体を実行しています。インタープリターが実行しているコードを実行していません。 Javaインタプリタは、コードを実行/解釈/実行するようにハードウェアに「伝える」ことではありません。そのタスク自体を実行しています。

...それを保存するコンパイラとは対照的に。

コンパイラの目的は、ある形式から別の形式にコードを「保存」しないで変換することです。例えば:

  • javactranslatesJavaソースコードをバイトコードに、
  • jITコンパイラはバイトコードをネイティブコードに変換し、
  • cコンパイラgcctranslatesCコードをネイティブコードに変換します。

たとえば、先ほど説明したインタープリターは、作成する言語でプログラムを実行するために使用され、JVMで実行されます-VMと見なされますか?

かもしれない。 VMが外界に公開されているかどうかに依存します。公開されていない場合は、仮想マシンと見なすかどうかについては議論の余地があります。

これは、他のプログラムを実行するためのプラットフォームとして機能します。これは基本的にVMが何であるかです。しかし、VMと見なされるためにさらに仕様が必要かどうかはわかりません。

仕様はありません。前にも言ったように。これらの用語のいずれにも仕様はありません。それらには従来の意味...のみがあり、単一の合意された定義はありません。

どうして?意味は時間とともに変化するからです。

辞書について考えてください。辞書の定義は単語を定義定義しますか?いいえ。彼らが実際に行っているのは、みことばのさまざまな一般的かつ歴史的な意味(複数形!)を文書化することです。また、異なる辞書は一致しない場合があり、辞書の定義は言語の変化を反映するために時間とともに変化します。


[〜#〜]フォローアップ[〜#〜]

私はこのようなものを意味しました:言語のコードは「hi」と印字されています。 Javaで書かれたインタプリタで解釈されます。したがって、インタプリタは新しい行 'System.out.println( "hi") `-同じ意味、異なる言語-で「出現」し、それを実行します。

いいえ。それはインタープリターが機能する方法ではありません。 「思いつく」プロセスはありません。あなたの例では、新しいJava作成されたコードはそれを行いません。むしろ、インタプリタ(Javaで書かれています)already操作を実行するためのコード、およびソース言語で表現できるその他の可能な操作を含みます...解釈されている言語。

(たとえば、Javaで記述されたインタプリタは、print "hi"のようなものを参照し、System.out.println( "hi"を実行します))。これは、後で基礎となるプラットフォームによって理解されるようにコンパイルされます-JVM)

いいえ。上記を参照。それはインタープリターが機能する方法ではありません。

実際、あなたが説明しているのは、インタプリタではなく、ソースコードからソースコードへのコンパイラのようなサウンドです。

コンパイラーはコード行を理解し、同じ意味の新しいコードを思いつきます-変換先の言語で-そしてそれをファイルに「保存」します。

部分的に正しいですが、ターゲットコードをファイルに保存する必要はありません。たとえば、JITコンパイラによって生成されたコードは通常メモリに書き込まれ、対応するメソッドが次に呼び出されたときに実行する準備ができています。

1
Stephen C