コンパイラとインタプリタ

機械語と高級言語とコンパイラ

ファイルとフォルダ」のページで、「コンピュータの中で、実際にプログラムを実行するのは、CPU(Central Processing Unit:中央処理装置)」と書きました。

ファイルとフォルダのページでnotepad.exeのファイルを紹介したように、プログラムもまたファイルとしてストレージに納められています。それをメモリに読み込んでCPUが実行します。そして、CPUが解釈し、実行できるのは、機械語だけです。

機械語というのは、実体としては単なる数字の羅列です(数字と言ってるのにAとかBとか出てくるのはなんで? と思った人は、こちら)。たとえばnotepad.exeから適当に抜粋したものが以下です。わけがわからないでしょう。私にもわかりません。

59 01 00 0F 1F 44 00 00 48 8B 4D E7 48 85 C9 74
0C 48 FF 15 B0 76 00 00 0F 1F 44 00 00 48 85 DB
74 0F 48 8B CB 48 FF 15 9C 76 00 00 0F 1F 44 00
00 48 8B 4D EF 48 85 C9 74 11 48 FF 15 87 76 00
00 0F 1F 44 00 00 EB 03 41 8A FD 48 8B 4D F7 48
85 C9 74 0C 48 FF 15 0D 59 01 00 0F 1F 44 00 00
48 8B 4D FF 48 85 C9 74 0C 48 FF 15 F8 58 01 00
0F 1F 44 00 00 40 8A C7 48 81 C4 98 00 00 00 41
5F 41 5E 41 5D 41 5C 5F 5E 5B 5D C3 CC CC CC CC
CC CC CC CC 48 89 5C 24 18 55 56 57 48 83 EC 40
48 8B F9 33 D2 48 8B 49 08 48 FF 15 D8 72 00 00
0F 1F 44 00 00 33 ED 85 C0 75 1A 48 FF 15 2E 73
00 00 0F 1F 44 00 00 3D B7 00 00 00 74 07 32 C0
E9 FC 00 00 00 48 8B 77 08 48 83 CB FF 48 FF C3
66 39 2C 5E 75 F7 4C 8D 43 10 33 D2 48 8D 4C 24
60 E8 BA EB FE FF 48 8D 53 10 4C 8B CE 48 8B 5C
24 60 48 8D 05 5F 8C 00 00 48 8B CB 48 89 44 24
20 4C 8D 05 70 8C 00 00 E8 83 20 FE FF 85 C0 0F
88 92 00 00 00 48 89 6C 24 30 45 33 C9 C7 44 24
28 82 00 00 00 45 33 C0 BA 00 00 00 C0 C7 44 24
20 02 00 00 00 48 8B CB 48 FF 15 E9 70 00 00 0F
1F 44 00 00 48 83 F8 FF 74 5D 48 8B C8 48 FF 15
54 72 00 00 0F 1F 44 00 00 48 8B 4F 08 48 8D 54
24 68 45 33 C9 45 33 C0 48 FF 15 01 72 00 00 0F
1F 44 00 00 85 C0 74 2F 48 8B 0D 89 2D 01 00 45

機械語はさすがに人間にはわかりにくいので、昔は、機械語と1対1に対応したアセンブリ言語(assembly language)でプログラムを書く、ということが広く行われました。アセンブリ言語は、CPUの種類により異なりますが、以下のような感じです。

        movl    $0, -4(%rbp)
        movl    $1, -8(%rbp)
        jmp     .L2
L3:
        movl    -8(%rbp), %eax
        addl    %eax, -4(%rbp)
        addl    $1, -8(%rbp)
L2:
        cmpl    $100, -8(%rbp)
        jle     .L3

こうやってアセンブリ言語で書いたプログラムを、手作業で(!)機械語に変換したり(ハンドアセンブルといいます※1)、アセンブラというプログラムで機械語に変換して実行していました。

まあ数字の羅列である機械語よりはマシですが、アセンブリ言語にしたって人間が書くのは大変です。そこで、もっと人間にとってわかりやすい言語でプログラムを書いて、それを機械語に変換すればよい、という発想が出てきます。そのような、高級言語を機械語に変換するプログラムのことを、コンパイラ(compiler)と呼びます。

上のアセンブリ言語は、「1から100までの数字を合計する」というプログラムなのですが、これを、たとえばCというプログラミング言語で書くと以下のようになります。

int i;
int sum = 0; /* 合計値を表す変数sumを0で初期化する */

/* 変数iを、最初は1にして、1ずつ増やしながら、100になるまで繰り返す */
for (i = 1; i <= 100; i++) {
    sum += i; /* sumにiを加える */
}

コメントを過剰に入れたとはいえ(Cでは、「/*」と「*/」で囲んだ部分がコメントです)、Cを知らない人でも、機械語はもちろん、アセンブリ言語と比べても、相当わかりやすいと思うのではないでしょうか。

Cのような、人間にとってわかりやすく作られたプログラミング言語を、高級言語とか高水準言語と呼びます。それに対し、機械語とかアセンブリ言語のような、機械にとってわかりやすい言語を低級言語とか低水準言語と呼びます。ここでの高級とか低級とかいうのは、あくまで技術用語ですので、高級言語の方が上等とか、そういう意味はありません。機械語やアセンブラを使って、ハードウエアに近いプログラムを書くことを低レベルプログラミングと呼ぶこともありますが、「低レベル」だからといって簡単なわけでもありません(むしろ難しいと思う)。

Cのような高級言語で書かれたプログラムのことを、ソースプログラム(source program)とかソースコード(source code)、あるいは単にソースと呼びます。ソースコードをテキストファイルとして保存したものがソースファイル(source file)であり、ソースファイルをコンパイルしてできる機械語の入ったファイルはオブジェクトファイル(object file)です。大規模なプログラムは複数のソースファイルに分割して書きますから※2、複数のソースファイルからそれぞれオブジェクトファイルができます。それをリンカ(linker)というプログラムがリンク(link:結合)して、ひとつの実行可能なプログラムにします。なお、リンクの際は、ライブラリ(library)という、誰でも使うような機能のプログラム(OSやプログラミング言語に付属しているものが多い)も結合されます。こうして出来上がるのが、実行可能なプログラムファイルで、Windowsなら拡張子が.exeになるのでEXEファイルと呼ばれます。

なお、上の図では、「ライブラリ」をリンカが丸ごとEXEファイルの中に埋め込んでいるように見えます。実際、そのようなリンク方法(スタティックリンク(static link)と呼びます)もあるのですが、現在のWindowsでは、多くのプログラムで共通して使うようなライブラリは、別ファイルに分けておいて、実行時にリンクします。こういうライブラリのことをダイナミックリンクライブラリ(Dynamic Link Library)、略してDLLと呼びます(DLLファイルの拡張子は.dllです)。ライブラリをそれぞれのEXEファイルに埋め込むよりも、DLLにした方が、ストレージを節約できます。

このように、ソースコードをコンパイラにより機械語のEXEファイルに変換する方式のことを、コンパイル方式と呼んだりします。

機械語は、CPUが直接解釈する言語なので、CPUの種類(アーキテクチャ)により異なります。今現在普及しているアーキテクチャは、Windowsパソコンのx64アーキテクチャ(IntelやAMDが作っています)、スマホとかでよく使われるARM、それから最近Macのためにアップルが独自開発したM1あたりでしょうか。

インタプリタ

コンパイル方式では、ソースコードをコンパイラが機械語に変換しました。別の方式として、インタプリタ方式という方法もあります。この方式は、インタプリタ(interpreter)というプログラムが、ソースコード(またはソースコードをちょっと変換した中間形式)を、解釈しながら実行する方式です。

前述のように、CPUは機械語しか実行できません。インタプリタというのは、「ソースコードを実行できるCPUを、プログラムで作り出したもの」と言えるでしょう。インタプリタはたいてい機械語にコンパイルされた普通の実行可能プログラムで、CPUはインタプリタを実行します。そして、インタプリタが、我々ふつうのプログラマが書いたソースコードを実行します。

図にすれば、コンパイル方式は以下のように事前にコンパイルした実行可能プログラムをCPUが直接実行しますが、

インタプリタ方式では、CPUが実行するインタプリタというプログラムが、ソースコードを解釈して実行します。

ただし、インタプリタでも、ソースを直接解釈するものは実はそう多くありません。Windowsなら、バッチファイルなどはソースを直接解釈する部類に入るでしょうが、たとえばRubyとかPythonといった言語は、ソースをいったんバイトコード(byte code)という中間形式に変換(これもコンパイルと呼びます)し、インタプリタが解釈するのはこの中間形式です。バイトコードは、実行の直前にメモリ中に自動で生成されるので、RubyやPythonを使う人には見えません。よって、人間からすれば、コンパイルの手間もかからず、インタプリタが直接ソースを解釈しているように見えます※3

Javaは、バイトコードをクラスファイル(class file)としてファイルに出力します。この方法では、コンパイルの手間がかかるわけですが、クラスファイルは、機械語の実行可能プログラムと違い、CPUやOSに依存しないので、異なるCPUやOSのマシンに持っていっても動作する、という利点があります。JavaのインタプリタをJVM(Java Virtual Machine:Java仮想マシン)と呼びます。

Java仮想マシンは「仮想マシン」なので、本物のCPUと同様、アセンブリ言語に相当するものがあります。上でも例に出している「1から100までの数字を合計する」プログラムを、Javaのアセンブリ言語で書くとこうなります(Javaで書いたプログラムからjavapで生成)。

       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_2
       5: bipush        100
       7: if_icmpgt     20
      10: iload_1
      11: iload_2
      12: iadd
      13: istore_1
      14: iinc          2, 1
      17: goto          4
      20: return

上で挙げた本物のCPU(x64と呼ばれるタイプ)のアセンブリ言語と、もちろん内容は違いますが、雰囲気は似ていると感じないでしょうか。JVMが「仮想マシン」と呼ばれる理由がわかると思います。

インタプリタ方式では、インタプリタという仮想マシン(仮想的なCPU)を本物のCPUが実行する、という2段構えになっていますから、処理速度は遅くなります(ものによりますが、だいたい数十倍くらいは)。そこで、最近の実用的な言語では、JITコンパイラ(Just In Time compiler、JITと略すことが多い※4)という機能を内蔵し、実行の直前にソースコード(またはバイトコード)を機械語に変換します。こうなってくると、インタプリタ方式もコンパイル方式も、実態としては似たようなものになってきているとは思います。

この入門で題材としているJavaScriptは、当初はインタプリタでしたが、今はJITを搭載しています。

なお、コンパイラとかインタプリタとか、こういったプログラミング言語を実行するためのプログラム一式のことを、言語処理系と呼びます。JavaScriptの言語処理系は、Webブラウザに内蔵されています。

インタプリタは翻訳しない

インタプリタ方式の説明として、「インタプリタ方式の言語では、実行時に1行ずつソースコードを機械語に変換します」という説明を見かけることがありますが、この説明は完璧に間違っています

上で説明したように、インタプリタというのは、ソースなり中間形式(バイトコードとか)を解釈しながら実行するプログラムです。インタプリタがソースから機械語への変換を行うことはありません。

最近の言語処理系はJITを搭載していることが多いので、そこでは機械語への変換が行われますが、JITはJust In Time コンパイラなのであってこの部分を指してインタプリタとは呼びません。

この間違いは、私が8ビットパソコンで遊んでいた40年近く前からある間違いですが、いまだにときどき見かけます。まだこの間違いをばらまいている書籍等があるのでしょうか。

バイトコードを実行するタイプのインタプリタは、素朴な実装では、バイトコードを先頭から読みながら、(Cで言えば)巨大なswitch caseで、バイトコードの命令(オペコードといいます)により処理を分けるような形になります。上の、Javaのバイトコードのアセンブリ言語での、iconst_0とかistore_1とかがオペコードです。その巨大なswitch caseを、バイトコードを先頭から順に読むためのループで囲んだものがインタプリタの本体です。

昔、私が作ったDiksamという言語のインタプリタは、以下のようになっています。Cがわかる人なら、これでインタプリタのイメージがつかめるのではないでしょうか。

http://kmaebashi.com/programmer/devlang/diksam_src_0_1_01/S/18.html#395

C#(.NET Framework)は?

C#というのは、マイクロソフトが開発したプログラミング言語です。

2002年にC#が世に出てきたとき、我々は思ったものです。「うわ、Javaのパクリじゃねえか!

C#は、見た目からしてJavaに似ています。もっともJavaだってCやC++に似ているわけですし、パクリが悪いわけではありません。プログラミング言語なんてこうしてパクリあいながら発展してきたものです(そして、その後の進化のスピードは、C#の方がJavaよりずっと上です)。

さておきC#は、言語としての見た目だけでなく、その実行形態もJavaと同様とされていました。Javaが、ソースをコンパイルしてバイトコードに変換し、JVMがそれを実行するように、C#は、ソースをコンパイルしてIL(Intermediate Language、CommonをつけてCILとも呼ぶ)というバイトコードに変換し、CLR(Common Language Runtime)という環境で実行する、というわけです。もっとも、C#は、後から出てきただけあって、最初からJITを前提にしていましたが。

ではVisual StudioなりでC#でソースを書いて、コンパイルすると、Javaでいうクラスファイルのようなファイルができるのかというと――なぜかEXEファイルができます。

C#(というか、.NET Framework)では、実行環境であるCLRは、clr.dllとかclrjit.dllとかのDLLとして提供されています。EXEの中には、コンパイルしてできたILがデータとして入っていますが、clrjit.dllに含まれるJITがそれを機械語にコンパイルして実行します。

公開日: 2021/06/20



次のページ | ひとつ上のページに戻る | トップページに戻る