最近の「インタプリタ」言語のインタプリタ事情 - コンパイラ言語・スクリプト言語との違い、JITコンパイラ、REPL、コンパイラ言語のスクリプト的実行サポートを徹底解説
PerlやRuby、Pythonなど様々なインタプリタ言語/スクリプト言語がありますが、最近だとインタプリタ言語もJITコンパイルしたり、コンパイラ言語の中にもスクリプトのように動かせるものもあります。 その辺りの今どきの事情について、エン・ジャパンの小澤が解説します。
最近の「インタプリタ」言語のインタプリタ具合
こんにちは。
エンジニアHubを運営しておりますエン・ジャパン株式会社のデジタルプロダクト開発本部(デジプロ)で、VPoEをしております、こざわと申します。
エンジニアHubはこれまで、社外の強いエンジニアの方々に記事を寄稿していただいていたのですが、今回から時々、エン・ジャパンのエンジニアも、記事を書かせてもらうことにしました。技術ブログをゼロから立ち上げるより、便乗した方がいい感じになるんじゃないか、という魂胆です。よろしくお願いします。
さて、弊社デジプロには、エンジニアではないWebディレクター、企画職の人たちの中にも
「RPAをがんがんやってみよう」とか
「Pythonを覚えて機械学習を走らせてサービス改善してみよう」といったことをしている人が結構いて、こざわ、結構これまで勤め先を転々としてきたのですが、あんまりそういう人はいなかったので、日々すごいなと思っております。
そんな風潮を後押しするべく、社内のみんなに「パソコン使って仕事するなら、何かインタプリタ言語一つ覚えておくと便利だよー、こざわはPerlだったし、10年前ならRubyだっただろうけど、今はPythonかなー。インタプリタ言語というのは…」と説明しようとして、ハタと困りました。今のインタプリタ言語、JITとかでコンパイルするやつもあるじゃん、なんて言えばいいんだ?と。コンパイラ言語でも、スクリプトっぽく動かせるものもあるぞ?あ、スクリプト言語って言えばいいのか?でも昔はその辺、ほぼイコールだったぞ、今どうなってんだ?…と。
世間の「ひとまず何かプログラミングを覚えてみよう」といった本を何冊か開いてみても、大体そこはお茶を濁した説明をすることが多いようなのですが、この機会に納得がいくまで調べてみることにしました。よかったら参考にしてください。
そもそもコンパイラ言語・インタプリタ言語とは。
こざわは20年前、大学でプログラミングを教わり始めた頃、先生から大体こんな感じで教わりました。
「コンピューターは自分のアーキテクチャに合った機械語で書かれたプログラム、つまりネイティブコードだけを実行することができる。 しかし、人間がそれを直接書くのは大変なので、人間の言葉(自然言語)に近い、色々なプログラミング言語が作り出された。 機械語に近いほど低級な言語、自然言語に近いほど高級な言語という。低級プログラマーと言ってもレベルが低いわけじゃない。むしろ腕のいい人が多い。」
「プログラミング言語で書かれたプログラム、つまりソースコードをコンピューターが動かすためには、どこかでそれを、ネイティブコードに『翻訳する』必要がある。事前に、ネイティブコードに『翻訳(コンパイル)』を済ませてしまい、翻訳できたそれを毎回動かすのが『コンパイラ言語』、実行するたびにソースコードを頭から一文ずつ逐次的に『解釈(インタプリト)』して、動かしていくのがインタプリタ言語だ。」
「コンパイラ言語の場合、実行は速くなるが、動かす前にコンパイルをするのが面倒。今はそれほどじゃないけど、昔はコンパイルにもそれなりに時間が掛かった。それに対して、さっと書いた物をさっと動かせるのがインタプリタ言語の良いところで、実行速度はコンパイラ言語に比べれば遅いけど、書いて直してすぐに動かせるのは大きい。特に対話的実行環境があると、色々試行錯誤しながらプログラムを作れたのですごく良かった。プログラムの書き方も、やりたいことを手短に書けるように設計されていることが多い。」
「コンパイルしたり、インタプリトしたりするプログラム、つまり、プログラムを動かすためのプログラムを処理系という。プログラムを書いたコンピュータと、プログラムを動かしたいコンピュータが違うとき、用意しなければいけない処理系がコンパイラ言語かインタプリタ言語かで異なる。コンパイラ言語だと、コンパイルした後のものを動かすので、それぞれの環境では、実行時に必要なライブラリさえあれば良い。ただし、そのライブラリは実行するコンピュータのアーキテクチャによって異なるし、コンパイル結果も実行環境に合わせて変えないといけない。一方で、インタプリタ言語は、動かしたいアーキテクチャでのインタプリタがそれぞれの環境に入ってさえいれば、同じソースコードを色々な環境に配って、そのまま動かすことができる」
などなど。今もその基本の所は変わっていないはずですが、
「インタプリタ言語なんだけど、実行速度を速くするため、実行を開始したら、その場でコンパイルを済ませてしまい、それを動かす言語」
「ソースコードから、別のプログラミング言語や、仮想的なアーキテクチャのための中間形式(バイトコード)にまで翻訳(コンパイル)しておき、実際に実行するためにはその中間言語や中間形式バイトコードを解釈して、それぞれのコンピュータに合わせて動かすインタプリタや仮想マシンを使う言語」
「コンパイラ言語なんだけど、コンパイルして実行する、というところをひとまとめにして使えるようにすることで、事前にコンパイルする手間を感じさせず、書いてすぐに実行したり、対話的環境を実現している言語」
などが出てきて、話がややこしくなっています。「対話的実行環境」については後ほど。REPLとかそういうやつです。
今現在の時点では、「動かす前に、より低級な言語・形式に変換しないといけない言語」をコンパイラ言語、それが不要な言語をインタプリタ言語としているようです。なるほど納得ですね。
スクリプト言語・P言語・LL言語・動的言語 とは
スクリプト言語という用語もあります。インタプリタ言語が「どのように実行されるか」に基づいた呼び名であったのに対して、スクリプト言語は「どのようなことに使うか」という別の区分けによる呼び名です。スクリプトというのは、「脚本」「台本」という意味で、大体次のような意味を込めての事のようです。
- 何かのプログラムを「役者」として操る「台本」「脚本」として使える。
- さっと書いてさっと動かして、さっと捨てる。
インタプリタ言語とスクリプト言語はほぼ同義的に扱われています。スクリプト言語のほぼ全てがインタプリタ形式で動くものであり、ほぼ重なっていたからです。
「プログラムを操るプログラム」の代表が、Bash、zshなどの「シェル」のうえで、プログラムを色々と呼び出すシェルスクリプトであったり、ブラウザの中で、ブラウザの機能を操るJavaScriptや、Microsoft Officeの中でその処理や操作を自動化するVBAでしょう。Windowsの「バッチファイル/PowerShell」や、MacのAppleScriptなどもそういったものです。特定の環境の中で、「他のプログラムを操る」「環境そのものであるプログラムを操る」ということに特化していて、逆に、それ以外のことをするには制限がかかっていたり、ちょっとやりにくかったりします。
一方で、元々はそういった「他のプログラムを操る、連係する」とか「テキストを処理する」など特定の用途を行う処理をさっと書ける言語として生まれたスクリプト言語の中にも、Perlなどのように、一通りのプログラムを何でも書けるように発展していったものや、最初からそのように作られたものがあります。
そういったものは、アプリケーション組み込みのスクリプト言語や、特定用途のスクリプト言語に対して、汎用スクリプト言語と呼ばれています。
また、PHP、Python、Rubyあたりは系統的には「Perlの嫌なところをどうにかしたい」アンチテーゼとして、しかしPerlから派生した言語として登場しました。作った人はもしかしたらそうではないというかも知れませんが、周りはそう見ています。たまたまですが、(Rubyを除いて)Pで始まる言語がが多かったので、この辺りを「P言語」と呼ぶことがあります。Rubyが入る場合も入らない場合もありますが、PもRも形は似ています。
このあたりの、汎用のスクリプト言語がLL言語という呼び名で呼ばれることもありました。
LLというのはLightweght
Language(軽量言語)の略なので、LL言語を日本語にすると「軽量言語言語」になります。まあ、「言語」が重なるのは「ナイル川」みたいなものです。C++やJavaのような言語が、コンパイルが必要で型がはっきりしていて、ちょっとした処理を書くにも色々な決まり事を書かないといけないといったところで、「重量的」であるとされたのに対して、「軽やかに使える」といった意味合いで使われていました。最近はあまり聞かなくなったような気がします。
また、「動的言語」については、これらのインタプリタ言語、スクリプト言語の「型」というものが、動的か静的かで言えば動的であることからついた呼び名です。これらの多くは、型というものが、何というかその…、なかったりとか、ゆるかったりとか、動的だったりとか…まあそういうものなのです。
型とは?とか、型が静的とか動的って何だ?という話は、手短に言えば型がコンパイルするときに決まるか、動かしてるときに決まるか、の違いなのですが、これはこれで絶対長くなる話なので、いつかそのうち気が向いたら別の記事にします。
対話的実行環境とは
スクリプト言語を動かすには、通常、シェルの中で
- ファイルにスクリプトを書き、それをインタプリタで動かす
- インタプリタに直接その言語のプログラムを渡し、即座に実行する(ワンライナー)
- プロンプトにプログラムの文を打ったら、その文を即座に動かしてくれて、結果を返してくれる。
というやり方を用意してくれている場合があります。そういうものを対話的実行環境といいます。
コンピューターとプログラマーが、プログラミング言語で対話するようにプログラムを実行する環境、という意味です。
プログラムを書きながら動かし、動かしながら書き、そういったことをよりやりやすくしたものです。
こういったものを作りやすいのはインタプリタ言語の特徴であるとされています。(が、最近はコンパイラ言語でも用意されてる場合がかなりあります)
ちょっとした計算をしたいときや、その言語でこう書いたらどう動くのか、ということを試したいときにとても便利です。
たとえばPythonの場合、こんな感じです。
% python Python 3.9.9 (main, Nov 21 2021, 03:16:13) [Clang 13.0.0 (clang-1300.0.29.3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> a=1 >>> a+a+a 3 >>> def plus1(n): ... return n+1 ... >>> plus1(a) 2
読み取り(Read)、評価(Eval)、印字(Print)をループ(Loop)するので、REPLとも呼ばれます。
主要スクリプト言語のコンパイル事情
さて、いよいよ本題で、主要(な気がする)インタプリタ言語で、今どれぐらいコンパイルをしているのかを見ていきましょう。ざっと調べてわかった範囲をまとめてみました。(間違ってたらすみません、直していきたいです。)
現代では、何かしらの形で、「実行を開始したら、その場で(ある程度の)コンパイルを済ませてしまい、それを動かす言語」 が主流です。 この、「その場でコンパイラを動かしてしまう方式」をJIT(Just in Time)コンパイルと言います。
一口にJITと言っても、中間コードにまで変換してくれる場合と、ネイティブコード(それぞれのPCのアーキテクチャごとのネイティブコードのコード)にまで変換してくれる場合とがあります。
Perl
かなり昔から、スクリプトを実行前に中間コード(構文木)にコンパイルしてから、それをインタプリタが実行をする方式を採っていて、これを指して「ランタイムコンパイルをしている」という場合があります。
なので、BEGINブロックという、コンパイル完了前に、読み込んで即座にコードを動かすための構文が用意されていたり、
構文エラーがスクリプトの後ろの方にあったりしても、頭の方が動く前にまずそれが検出されてエラーになったりします。
一回コンパイルされてから動くという知識がこの辺りの挙動を理解するためには重要です。
Python
Pythonも、実行のタイミングで、スクリプトをまず抽象構文木に、つづいて抽象構文木をバイトコードにコンパイルしてから、バイトコードを解釈する仮想マシン(インタプリタ)で動かしています。
標準実装であるCPythonはそのような折衷的なインタプリタ方式で動いていますが、NumbaやCythonなど、拡張ライブラリや標準とは別の実装で、さらに高度なコンパイルができて、高速化が図れるようになっていたりします。
Ruby
2018年に正式リリースされたRuby 2.6.0より、JITコンパイラが導入され、これを有効にすると性能が向上するようになりました。
一回C言語ソースに言語ソースに変換し、それをC言語コンパイラがコンパイルして、ネイティブコードとして動かせるようになっています。
PHP
PHP7まではソースコードを中間コード(opcode)に変換してから、それをインタプリタ(仮想マシン Zend VM)が実行する方式でした。その中間コードをキャッシュする仕組み(opcache)もありました。
2020年メジャーアップデートのPHP8から、ネイティブコードにまでコンパイルしてくれるJITコンパイラが使えるようになりました。
JavaScript
各種ブラウザごとにスクリプトエンジンがあるわけですが、方式はそれによって様々です。
たとえば、ChromeやNode.jsが使っているGoogle製の v8 エンジンでは、
- 最適化はしないけど、素早くネイティブコードにしてくれるfull-codegenコンパイラ
- コンパイラにちょっと時間は掛かるけど、最適化をしたネイティブコードにしてくれる crankshaft コンパイラ
と、二つのJITコンパイラを使い分けて、かなり賢いコンパイルをしてくれているみたいです。
コンパイラ言語のREPL/スクリプト的実行サポート
一方で、今どきのコンパイラ言語も、そちらはそちらで、スクリプト言語的に手早く書いたコードを動かすことができるようになってきています。それをいくつか見てみましょう。
JVM言語
Javaに始まるJava VM(JVM)を使った言語のファミリーです。スクリプト言語的な性格は結構差があります。
Java
一昔前はjavac Hello.java
みたいな感じでソースファイルをJVM用バイトコードにコンパイルしてjava Hello
コマンドでそれを実行というのが当たり前でした。
更に、JVMでのバイトコードのバイトコード実行を速くするために、バイトコードをネイティブコードにしてくれるJITコンパイラが導入された経緯があります。
そういった点で、インタプリタ言語と、コンパイラ言語の中間的な性質を持っているのですが、実際、これを第三のVM方式の言語と分ける分類もあります。
最近の変更で、「さっと動かす」ことがやりやすくなってきました。
- JDK9から、対話型実行環境であるJShellが登場しました。REPL方式で、Javaを動かすことが出来ます。
- また、JDK11から、「ソースファイルが一つだけの場合」に限って、
java
コマンドから Javaソースファイルを直接コンパイルして実行できるようになりました。
Scala
Javaと同じく、scalac Hello.scala
でコンパイルして、scala Hello
で実行という流れです。しかし昔から、単に
scala
コマンドを打つと、REPL環境が立ち上がり、そのばでScalaコードを書いて動かすことが出来るようになっていました。
Kotlin
kotlinc Hello.kt
でコンパイルして、kotlin
か
java
コマンドで動かす流れになりますが、Kotlinのためのランタイムを含めたjarファイルにして、java
コマンドで動かす場合が多いように思います。
% kotlinc Hello.kt -include-runtime -d hello.jar % java -jar hello.jar
あるいはkotlinc
コマンドに-script
オプションを付ければ、スクリプトのようにKotlinファイルを動かすこともできます。コンパイルと実行を一辺にやってくれるわけです。(その場合、拡張子を kts
にします。)
% kotlinc -script Hello.kts
Scalaと同様、 kotlinc
コマンドを単独で使うと、REPLが立ち上がります。
Groovy
元々、Javaをよりスクリプトっぽく使えるようにしようという言語です。
groovy Hello.groovy
でスクリプト的に実行、groovyc Hello.groovy
でコンパイル、groovy Hello
でコンパイルした
.class
ファイルを実行してくれます。スクリプトっぽく動かすところを優先した設計になってますね。
くわえて、groovysh
コマンドでREPLが立ち上がります。
Go
コンパイル言語で、ネイティブコードまでコンパイルしてくれるのですが、
% go run hello.go
で、ソースをそのまま実行することも、
% go build hello.go
でコンパイルして、実行ファイルを作り、
% ./hello
のようにそれを実行したり、両方の使い方ができます。モダンですね。
一方で、標準実装には含まれないパッケージですが、gore
という対話型環境があり、REPLなこともできます。
まとめ
一口にJITコンパイラと言ってもそれがやってくれることが違ったり、今どき純粋に上から一行ずつ逐次的にインタプリトしている言語は少なかったり、ガチガチのコンパイラ言語だと思っても、REPLが充実していたり、色々調べて分かってよかったです。
そうなってくると「コンパイルしてるから速い」「コンパイルしてないから遅い」みたいな事でもなさそうな雰囲気です。
性能計測は、ちゃんと測ってやりましょう(教訓)。
Masayuki Kozawa