インタプリタ
プログラミング言語で書かれたソースコードないし中間表現を逐次解釈しながら実行するプログラム
From Wikipedia, the free encyclopedia
仕組み
インタプリタは各仮想命令に紐づく命令ボディとディスパッチ機構をもち、ホストマシン上で実行可能になっている(例: x86マシンコードbinファイル)。仮想命令群からなるコードを引数としてインタプリタが実行されると、仮想命令に基づいて制御が移行され対応する命令ボディが実行される。これを繰り返すことでインタプリタはコードを実行する。
命令ボディ
命令ボディ(英: instruction body、命令本体とも)は仮想命令をホスト言語でエミュレートするコードである[2]。命令ボディがホストマシンで実行されることで仮想命令が事実上実行される。すなわち命令ボディは仮想命令の実装である。例えば二項和仮想命令 ADD2 に対応してC言語で書かれた push(pop() + pop()) は命令ボディである。
ディスパッチ
ディスパッチ(英: dispatch)は仮想命令に基づいた命令ボディから命令ボディへの制御移行メカニズムである[2][3]。ディスパッチの最もシンプルな例はSwitch文(Jump命令)である。ある仮想命令の実行後、次の仮想命令に対応する命令ボディへ制御を移す(Jumpする)ことでその仮想命令実装が実行される。
コンパイラとの違い
プログラミング言語処理系の実装には、インタプリタとコンパイラの2つがある、とされてきた。インタプリタは実行を行うが、コンパイラは実行を行わない、という差がある。
インタプリタは、インタプリタが提供する言語のプログラム文を1文ずつ、インタプリタを実装した言語の機能を呼び出していく単純な方式が一般的であった。コマンドラインインタプリタではこれに加えて、別のプログラムに処理を委ねて一つの機能を実現する。
この時代のインタプリタの長所と欠点については、およそ次のような解説がされることが一般的であった。
- (長所)プログラムを作成している途中でも、とりあえず書かれた箇所まで実行させることができ、プログラマの期待通りの動作をしている場合も、期待通りの動作をしていない場合も、早期にそれを確認・発見し、そして修正後すばやく実行、再確認できる。
- (短所)実行速度が遅い。ループ(=繰り返し)の箇所などでは1度構文解釈した文でも、毎回あらためて解釈と実行を行うので、コンパイラ方式に比べて実行速度が遅くなる。ループを全く含まないような、全ての命令が一回だけ解釈され、一回だけ実行されるような(ある意味、特殊なプログラムであれば、解釈+実行のトータルの時間は、インタプリタでもコンパイラでもさほど差は生じない。だが、実用的なプログラムは一般的に、多数のループを含んでおり、そうしたプログラムではインタプリタのほうが実行を完了するまでの時間が多くかかり、特に、ループ回数が多ければ多いほど古典的なインタプリタの相対的な遅さは顕著になってくる。→#長所と短所
そうした欠点を解消すべく、1990年代頃には毎回高水準言語から機械語に変換するのではなく、中間言語に変換することで高速化をはかるインタプリタなどが作りだされた。 そのような改良の結果、古典的な意味での「インタプリタ」と「コンパイラ」の双方の性質を備えたようなインタプリタが登場し、複雑化してきている[注 1]。
近年のインタプリタがおこなう、コンパイラが行うような変換のひとつとしては、高速化などを目的とした、実行時コンパイラによる動的コンパイルを挙げることができる。
動的プログラミング言語のようにインタプリタでの実装が向いている方向性の言語、あるいはその逆もある。
歴史
インタプリタという手法、すなわち、「そのハードウェアが直接解釈するのではないプログラム」を受け取り、「プログラムで実装された抽象的な、あるいは仮想上のコンピュータで解釈実行する」というプログラムの実行法は、コンピュータが登場した時から、ないしそれ以前からある。
万能チューリングマシンは、「どんなチューリングマシンについても、それを模擬できるチューリングマシン」というもので、ある種のエミュレータないしインタプリタであり、考察されたのは電子式のコンピュータの誕生する以前である。
実用的な機能を持ったプログラム内蔵方式の世界初の電子計算機とされている、EDSACにおいて既に、ある種のインタプリタが実装されていたことが記録に残っている。同機におけるプログラミングの技法が書かれたThe Preparation of Programs for an Electronic Digital Computerのchapter 2の§ 2-22 Interpretive subroutinesで説明されているが、複素数演算などのサブルーチンを明示的にサブルーチンとして呼ぶのではなく、通常の加減算などと同様の形式のプログラムをインタプリタで解釈してそれらのサブルーチンを利用する、というものである。また日本においても、パンチカードを入力としてパッチパネルの配線によるプログラミングで処理するような機械で、配線によってある種のインタプリタのようなものを実装し、パンチカードの内容をデータとしてではなくプログラムのように扱う、というような例があると言われている[4]。
最初のLISPインタプリタはスティーブ・ラッセルがIBM 704上に実装した。これはジョン・マッカーシーがRecursive Functions of Symbolic Expressions and Their Computation by Machine, Part Iで数学的に示したものであったが、マッカーシー自身は実装できるとは考えていなかった。しかし、この論文を読んだ当時大学院生であったラッセルは実装可能だと言い、数学的な記述から変換して機械語で実装してみせたという[5][6]。
1960年代には、プログラミング言語から中間表現にコンパイルし、それをインタプリタで実行する、現在のJVMなどと同様な手法も一般的になった(pコードマシン)。
長所と短所
開発時に修正作業が容易
プログラム開発中、プログラマは頻繁にソースコードに手を加える。コンパイラの場合、ソースコードを変更するたびにコンパイルし、リンクして実行ファイルを完成させないと、そのプログラムを実行できない。プログラムが大きくなると、ビルドの完了を待っている時間が長くなる。一方、インタプリタではソースコードをそのまま実行するか中間表現に変換するだけなので、ほとんど待つ必要がなく、修正がうまくいったかどうかのテストをより素早く確認できる。
この特性を利用したプログラムとしてREPLがある。入力を受け取る(Read)、入力の評価(Eval)、評価結果を提示する(Print)を繰り返す(Loop)、テスト環境の一つである。評価はインタプリタに限らずコンパイラで行なわれるかもしれないが、ユーザからの入力は常にソースコードの断片であり、それを逐次実行するという点で一種のインタプリタということもできる。実際、その挙動はソースコードを指定せずに起動したBASICインタプリタなどのそれとよく似ている。しかし、テスト環境も兼ねているため、エラーが起きた場合のメッセージが詳細にしてあることもある。関数型言語や論理型言語では、通常は機械語にコンパイルする処理系であっても用意される場合が多い。
可搬性
事前コンパイラ方式では、事前に生成された機械語の実行ファイルがユーザーに配布される。機械語はプロセッサアーキテクチャに依存するため、可搬性が低く特定のプロセッサでしか動作しない。
一方インタプリタ形式ではソースコードとインタプリタが配布される。インタプリタは機械語実行ファイルであるため環境に依存するが、ソースコードには環境に依存しない言語を採用できる。その場合環境に合わせたインタプリタを事前配布しておけば、アーキテクチャに依存しないプログラムの配布が可能である(可搬性が高い)。またインタプリタの代わりにJITコンパイラ(実行時コンパイラ)を用いても同様の利点が得られる。同一動作の保証はインタプリタ実装に依存しており(JITであればコンパイラ)、互換性バグの例として表計算マクロやWebページ(HTML)が挙げられる。
複雑性
事前コンパイラ方式では1つのバイナリを実行するだけでプログラムが機能する。一方インタプリタ方式では、まずインタプリタをインストールしその上でソースコードを動かす必要がある。その結果全体のプログラムサイズが大きくなり、ユーザーの手間が増える場合がある。またJITコンパイル(実行時コンパイル)方式でも同様の複雑性が生まれる。
可読性
インタプリタ用コード(高水準言語コードあるいはバイトコード)は機械語バイナリより容易に解読できる。ゆえに配布後のデバッグや修正が容易な一方、知的財産保護上の問題を起こしうる。そのための暗号化・難読化を考慮した言語・システムが存在する。またJITコンパイル方式でも同様の可読性に関する特性が現れる。
速度
インタプリタ方式の実行速度はコンパイラ方式の実行速度よりも遅い傾向がある。
事前コンパイラではプログラム内の文の解析を実行前に1回だけ行うが、単純な実装のインタプリタではそれを文ごとに実行時に毎回行うため、実行性能が低くなる。単純な実装のインタプリタでは変数にアクセスする際も、識別子とメモリ上の位置のマッピングを確認しなければならず、しかもそれを実行中に何度も行わなければならない。
また、ディスパッチはインタプリタが本質的に抱えるコストである。事前コンパイラ方式ではコンパイル時にディスパッチ相当の命令ボディ整列がおこなわれるため、実行時にディスパッチコストが発生しない。ゆえにインタプリタ方式ではディスパッチコスト×命令数分の追加コストが本質的に掛かる。
ディスパッチは分岐予測を難しくする。ゆえにパイプライン方式を採用するCPUにおいて速度へ影響を与える[7]。影響の大きさは分岐予測器の性能に左右され、2000年代以前のCPUではインタプリタの低速度がこのペナルティによって引き起こされるとされていた。予測器の性能が上昇した2010年代以降のCPU、たとえばHaswellマイクロアーキテクチャ (x86) ではその影響は小さいとされる[8]。
インタプリタによる開発の速さとコンパイラによる実行の速さの間で、様々な妥協案が考案されてきた。一部のLISP処理系などでは、インタプリタのコードとコンパイルされたコードが相互に呼び出しあうことができ、変数も共有できる。そのため、あるルーチンをインタプリタで評価しデバッグした後、先行してコンパイルして実行性能を高めつつ、他のルーチンを開発し続けることができる。多くのインタプリタはソースコードをそのまま実行するわけではなく、よりコンパクトな内部形式に変換している。多くのBASICインタプリタは予約語を1バイトのトークンに置換し、それをジャンプテーブルのインデックスとして使用する。PBASICなど一部のインタプリタでは、バイト単位ではなくビット単位でプログラムの短縮を行っており、例えばコマンドを5ビットで表し、一般に16ビットで表される定数をその数値の大きさに対応して可変長(3、6、10、18ビットなど)で表し、アドレスオペランドとして「ビットオフセット」を用意している。多くのBASICインタプリタは独自にトークン化された内部表現を保存し、読み込むことができる。
インタプリタがコンパイラと同様の字句解析と構文解析を行い、その結果生成された抽象構文木を解釈することもある。
プログラムの実行時間はコンパイラよりもインタプリタの方が長いが、実行回数が極端に少ない場合、コンパイル時間と実行時間を合計すればインタプリタでの実行時間よりも長くなることがある。プロトタイピングとテストにおいては、この差が重要となる。
バリエーション
スレッデッドコード
それ自体はインタプリタの手法とも言えるしコンパイラの手法とも言える。そのどちらと言うよりも、中間表現の1種類というべきかもしれない。仮想関数テーブルやテーブルジャンプによるフロー制御に似ていなくもない。
スレッデッドコードは、呼び出されるべきサブルーチンのアドレスのみが順番に羅列されたものである。「直接スレッディング」の場合は、そのアドレスが指す先は機械語のサブルーチンである。他にもいくつかのバリエーションがある。「サブルーチン・スレッディング」は最も違うタイプのバリエーションで、アドレスのみではなく、機械語のCALL命令として羅列するので、ハードウェアのプロセッサで直接実行できる。これは実行のオーバーヘッドは極小だが、メモリ効率は悪い。サブルーチン・スレッディング以外のスレッデッドコードは、きわめて単純なインタプリタで実行できる。Forthでは「内部インタプリタ」と呼んでいる。これは、Forth言語自体を実装しているインタプリタである「外部インタプリタ」と対になっている。
バイトコード
ソースコードを実行可能な形にするには、まず、ソースコードを構文木に変換する必要がある。構文木のまま、インタプリタ型の処理系で実行する処理系もあるが、構文木をさらにバイトコードなどの中間表現に変換してから実行する物もある。中間コードをバイトコードと呼んでいる処理系では、そのインタプリタをバイトコードインタプリタと呼ぶ。Javaプラットフォームや.NET Frameworkのように、中間コードの仕様を公開しファイルに書き出すものもあるし、仕様は公開せず処理系内部だけで使用するものもある。動的コンパイルを使っているインタプリタは、内部で実機の機械語に変換し実行する。
インタプリタとコンパイラの間には様々な中間的実装が存在し、それぞれにプログラム実行前に行われる解析の度合いが異なる。例えばEmacs Lispはバイトコードにコンパイルされ、LISPのソースを高度に圧縮し最適化した表現にしているが、それは機械語コードではない(したがって特定のプラットフォームに依存しない)。この「コンパイル」されたコードを解釈するのがバイトコードインタプリタである(それ自体はC言語で書かれている)。この場合のコンパイルされたコードは仮想マシンの機械語コードであり、仮想マシンはハードウェアで実装されておらず、バイトコードインタプリタとして実装されている。同様の手法はOpen Firmwareシステムで使われているForthコードでも使われている。ソース言語は「Fコード」(バイトコードの一種)にコンパイルされ、それを仮想マシンが解釈実行する。他にPコードマシンなどがある。
コントロール・テーブルはコンパイラを通さなくとも生成でき、バイトコードインタプリタと同様の方法でカスタマイズされたインタプリタでの適切なアルゴリズム的制御構造を記述できる。
抽象構文木
インタプリタとコンパイラの中間的手法の1つとして、ソースコードを最適化された抽象構文木 (AST) に変換し、その木構造にしたがってプログラムを実行するか、JITコンパイラでの機械語コード生成に使用する方法がある[9]。この方式では各文を1回だけ構文解析する必要がある。バイトコードに比べると、ASTではプログラムの全体的構造や文と文の関係を保持でき(それらはバイトコードでは失われる)、圧縮するとさらにコンパクトな表現になる[10]。そのため、実行時コンパイラにとってはバイトコードよりもASTの方が優れた中間表現だとして提案されてきた。また、実行時の解析もより優れたものにできる。
しかし、ASTはバイトコードよりも冗長であるため、インタプリタとしてはオーバーヘッドが大きくなるという問題がある[11]。CRubyの場合は1.8までは構文木インタプリタであったが、1.9では(開発中にはYARVと呼ばれていた)バイトコードインタプリタに入れ替えられ、性能が向上した。
実行時コンパイル
インタプリタとコンパイラの境界をさらにぼやけさせる方式として、中間表現をJITコンパイラでコンパイルし、実行時にネイティブの機械語にコンパイルする技法がある。これはネイティブなコードの実行効率を実現する代わり、ASTやバイトコードを最初にコンパイルする際に起動時間やメモリ使用量が増大するという欠点がある。これを補う技法として適応的最適化があり、インタプリタが実行中のプログラムを性能解析して最も頻繁に実行される部分をネイティブのコードにコンパイルする。これらの技法は1980年代のSmalltalkなどの言語で使われ始めた[12]。
実行時コンパイルは近年多くの言語処理系で採用されており、Javaプラットフォーム、.NET Framework、最近のJavaScriptの実装でもJITが採用されている。
トランスレータ方式
他のインタプリタ言語に変換して、ターゲット言語のインタプリタ上で実行する方式。例えばCoffeeScriptはJavaScriptに変換されて、JavaScriptインタプリタ上で実行される。
応用
デバッグ、教育用
パンチカード
パンチカードシステムにおいて、パンチカードを読み込んで、その内容を人間が読める形式(文字)でパンチカード上に印字する機械をインタプリタと呼ぶ。例えば、IBM 550 Numeric Interpreter (1930年) や IBM 557 Alphabetic Interpreter (1954年) がある。
インタプリタ型言語
これらは標準実装、もしくは一般に利用される実装がインタプリタで提供されている、一般的にインタプリタで利用されることが多いプログラミング言語である。しかし、これをもって必ずインタプリタで実行される言語であるとすることはできない。
- ActionScript
- APL
- AWK
- Bash
- BASIC(古典的なマイクロコンピュータ用)
- CFScript
- Common Lisp
- Csh
- Curl
- dBASE
- Emacs Lisp
- FoxPro
- Hot Soup Processor
- HyperTalk
- Io
- Java FX Script
- JavaScript
- Mathematica
- MATLAB
- Perl(第5版まで[要出典])
- PostScript
- Prolog
- REXX
- Ruby
- Python
- PHP
- R言語
- S言語
- Scheme
- SKILL
- Tcl
- TeX
- VBScript
- XSL
- Windows PowerShell
- なでしこ
- ひまわり