言語処理系一般の仕組みを腑分けして、JVM言語の動き方・動かし方を把握します。
• 言語処理系の構成 • コンピュータはどのようにプログラムを動かすのか • JVMはどのようにプログラムを動かすのか • JVM言語処理系の構成: Kinkを題材に
本資料をCC BY 4.0ライセンスにもとづいて提供します。 https://creativecommons.org/licenses/by/4.0/deed.ja
JVM言語の動き方・動かし方2019-02-27 JJUGナイトセミナーハッシュタグ: #jjug宮川 拓
View Slide
⚫ @miyakawa_taku⚫ JJUG幹事です⚫ Salesforceで働いてます⚫ 奄美出身の力士を応援しています☆⚫ オレオレJVM言語Kinkを作っていますhttp://doc.kink-lang.org/自己紹介#jjug2/127
背景Graal+Truffleは素敵みたいGraal+Truffleの素敵さを納得するため、言語処理系のかんどころを分かっときたい#jjug3/127
演目言語処理系の構成コンピュータはどのようにプログラムを動かすのかJVMはどのようにプログラムを動かすのかJVM言語処理系の構成:Kinkを題材に#jjug4/127
演目言語処理系の構成コンピュータはどのようにプログラムを動かすのかJVMはどのようにプログラムを動かすのかJVM言語処理系の構成:Kinkを題材に#jjug5/127
そもそも「言語処理系」ってなに?#jjug6/127
本セッションにおける用語定義書かれたプログラムくりかえし 三回:念仏.唱えるコンピュータ上での実行南無阿弥陀仏南無阿弥陀仏南無阿弥陀仏この間に必要なソフトウェア一式を「言語処理系」と呼ぶことにする#jjug7/127
言語処理系の二段階翻訳系 *1 実行系 *1書かれたプログラム実行に適した表現:機械語 orバイトコード or ...実行に適した表現をコンピュータ上で実行#jjug8/127
翻訳系ここはザックリ行きます#jjug9/127
翻訳系書かれたプログラム実行に適した表現:機械語 orバイトコード or ...つまりコンパイラ#jjug10/127
翻訳系の中身書かれたプログラムトークン列抽象構文木 (AST)実行に適した表現字句解析構文解析シンボル解決, 型検査, 最適化,,," く り か え し ", " 三 回 ", " : ", , ,(loop 3 (call (var "念仏") "唱える"))#jjug11/127
つまり翻訳系は多段階のデータ変換処理プログラマの多くは、似たような処理を作った経験があるはず#jjug12/127
データ投入バッチタブ区切りテキストトークン列抽象構文木 (AST)データベース字句解析構文解析マスタ突合, バリデーション,,,"中島敦", "悟浄歎異", 1942, 改行,,,[{"author": "中島敦","title": "悟浄歎異","year": 1942},,,]中島敦 悟浄歎異 1942深沢七郎 風流夢譚 1960,,,やってることは翻訳系と同じ!#jjug13/127
翻訳系でやっていることは、とても馴染み深いデータ変換処理ただし、型検査や最適化などは、それぞれが一大トピック#jjug14/127
実行系こっちが今日のメイン#jjug15/127
実行系実行に適した表現をコンピュータ上で実行#jjug16/127
誰が実行する?実行対象 実行の担い手機械語のプログラムCPUとOSつまりコンピュータそのもの機械語以外:バイトコード、木構造など実行系のプログラム:JVM, V8, YARVなどコンピュータの真似をする“Java Virtual Machine”!#jjug17/127
ここから先は、◼ コンピュータがどのようにプログラムを動かすのか◼ JVMやJVM言語処理系が、どのようにコンピュータを真似するのかを見ていきます#jjug18/127
演目言語処理系の構成コンピュータはどのようにプログラムを動かすのかJVMはどのようにプログラムを動かすのかJVM言語処理系の構成:Kinkを題材に#jjug19/127
サンプルプログラムN = M + 42ld x5, &Maddi x6, x5, 42sd x6, &N書かれたプログラムRISC-Vの機械語プログラム x5, x6: レジスタ&M, &N: 変数の番地#jjug20/127
OSの仕事: ローダ#jjug21/127
OS(ローダ)の仕事 1/2プログラムが使う仮想アドレス空間を確保機械語命令列の領域グローバル変数の領域100番地200番地#jjug22/127
OS(ローダ)の仕事 2/2プログラムをアドレス空間にロード *2#jjugld x5, &Maddi x6, x5, 42sd x6, &N...MN...100番地200番地204番地23/127
CPUの仕事: 計算#jjug24/127
CPUCPUの概念図 *3レ ジ ス タ 群演 算 装 置( ALU )メ モ リデ ー タ 用ポ ー ト命 令 用ポ ー トProgramCounter+4番 地命 令番 地入 力 演 算 結 果デ ー タ次 の 命 令 へレ ジ ス タ : 計 算 に 使 う 数 値 を 置 い て お く 場 所Program Counter ( PC ) : 実 行 す る 命 令 の 番 地 を 表 す 特 殊 な レ ジ ス タ演 算 装 置 ( ALU ) : 加 算 器 と か 除 算 器 と か の 回 路 の か た ま り#jjug25/127
CPUld x5, &Mレ ジ ス タ 群演 算 装 置( ALU )メ モ リデ ー タ 用ポ ー ト命 令 用ポ ー トProgramCounter+41) 1006) x57) +43) &M=200 4) &M=2005) M=31) 実行する命令の番地2) 命 令5) M =3 をロード#jjug2) 命令をロード3, 4) M の番地6) 3 をx5 レジスタに書き込み7) +4 して次の命令の番地へ26/127
CPUaddi x6, x5, 42レ ジ ス タ 群演 算 装 置( ALU )メ モ リデ ー タ 用ポ ー ト命 令 用ポ ー トProgramCounter+41) 1043) x58) +45) 421) 実行する命令の番地2) 命 令5) 即 値 をALU に入力4) x5=3 6) 和=457) x6#jjug3, 4) x5 =3 をALU に入力2) 命令をロード 6, 7) 和をx6 レジスタに8) +4 して次の命令の番地へ27/127
CPUsd x6, &Nレ ジ ス タ 群演 算 装 置( ALU )メ モ リデ ー タ 用ポ ー ト命 令 用ポ ー トProgramCounter+41) 1085) x67) +43) &N=204 4) &N=2046) x6=451) 実行する命令の番地2) 命 令5, 6) x6 =45 をN に書き込み#jjug3, 4) N の番地2) 命令をロード 7) +4 して次の命令の番地へ28/127
CPUの仕事: 手続き呼び出し#jjug29/127
手続き呼び出し高度なプログラムでは、手続きの呼び出しによって処理があちこちにジャンプするfoo {...bar()...}bar {...return;}callreturn#jjug30/127
手続きbarを呼び出すjal x1, &barjal命令は次の2つを実行する1. PC+4をx1に入れる⚫ PC+4は、jal命令の次の命令の番地⚫ あとでこの戻り番地に戻ってくる2. PCに&barを入れる⚫ つまり、barの命令列にジャンプする#jjug31/127
手続きbarから戻るjalr x0, x1#jjugjalr命令は次の2つを実行する1. PC+4をx0に入れる⚫ x0は、入れた値が捨てられる特殊なレジスタ⚫ つまりここでは、PC+4は捨てられる2. PCにx1を入れる⚫ x1には、呼び出し元で設定された戻り番地が入っている⚫ つまり、呼び出し元にジャンプ=returnする32/127
OSの仕事: スタックの確保#jjug33/127
呼び出し先でレジスタの値が変えられると、元の処理が続けられないので困る呼び出し前後で、レジスタの元の値を退避しておく領域が必要→ これがスタック領域#jjug34/127
スタックの確保スレッドXのスタックOSはスレッドごとにスタックを確保する#jjugスレッドYのスタックスレッドZのスタック35/127
スタックの中身fooのフレームbarのフレームcallしたら呼び出しの作業領域として「フレーム」を確保するreturnしたらフレームを捨てる#jjugスタックの中身のやりくりはプログラムの責任36/127
OSの仕事: 動的メモリ確保#jjug37/127
動的メモリ確保OSは実行中のプログラムの要求に応じて、仮想アドレス空間を動的に割り当てる#jjug38/127
小まとめ:コンピュータはどのようにプログラムを動かすのか#jjug39/127
プログラムを動かすためにコンピュータは◼ プログラムのロード◼ 計算◼ 手続き呼び出し&スタック管理◼ 動的メモリ確保などを行います#jjug40/127
演目言語処理系の構成コンピュータはどのようにプログラムを動かすのかJVMはどのようにプログラムを動かすのかJVM言語処理系の構成:Kinkを題材に#jjug41/127
Java処理系の二段階翻訳系: javac 実行系: JVMJavaソースコードJavaバイトコードJavaバイトコードをコンピュータ上で実行#jjug42/127
実行系はコンピュータの真似をするでも、まったく同じことをする必要はない!◼ 抽象度の高い枠組みを提供しても良い◼ コンピュータの機能が使えるのであれば、そのまま使えば良い◆ 加算器の回路をシミュレートするより、add命令を使う方が良い#jjug43/127
コンピュータ, JVM,WebAssemblyの比較#jjug44/127
WebAssembly各種言語をブラウザ上で動かすための実行系の仕様JVMと位置づけは似ているが、構成はかなり異なる#jjug45/127
オブジェクトシステムコンピュータWebAssemblyJVMオブジェクトシステムは存在しないJavaクラス&インスタンス#jjug46/127
メモリ管理コンピュータWebAssemblyJVMフラットなアドレス領域を割り当てGCは存在しないオブジェクトの生成を管理参照関係を追跡して、不要なオブジェクトをGC#jjug47/127
手続きの呼び出しコンピュータWebAssemblyJVMアドレスへのジャンプクラスの仮想関数テーブルをルックアップ&ジャンプまたはinvokedynamicで実行時に呼び出し方法を規定#jjug48/127
スタックコンピュータWebAssemblyJVM他の呼び出しのフレームも読み書き可能現在の呼び出しのフレーム以外は読み書きできない#jjug49/127
計算コンピュータWebAssemblyJVMPC, ALU, レジスタ, ...仕様は項書換え系実装は様々バイトコードインタプリタ&JITコンパイラ計算にはさまざまな実現方法がある#jjug50/127
計算: 項書換え系#jjug51/127
項書換え系ルールに従って項(式の一部)を書き換える(1+2) * 33 * 39項書換え系ですべてのプログラムが表せる!→ ラムダ計算、SKIコンビネータ計算#jjug52/127
項書換え系利点◼ データと演算をひとまとめにするので理論的に扱いやすい◼ 数学っぽくてかっこいい欠点◼ 素直に実装すると遅い#jjug53/127
計算: Metacircular Interpreter#jjug54/127
Meta-circular InterpreterASTやそれに類する木構造を再帰的に手繰って計算を適用する「ASTを直接評価」という場合はこれのこと#jjug55/127
Meta-circular Interpreter利点◼ ホスト言語(実行系を記述する言語)の制御構造が使えるため、実装が楽欠点◼ ホスト言語の制御構造に制約される#jjug56/127
計算:バイトコードインタプリタ#jjug57/127
バイトコードインタプリタ命令列の実行をホスト言語上でエミュレート命令の抽象度は機械語よりも高い◼ JVMの場合:◆ new命令: インスタンスを生成◆ getfield命令: オブジェクトからフィールドの値を取得#jjug58/127
バイトコードインタプリタ実装のイメージfor (int pc = 0; pc < instructions_len; ++pc) {long insn = instructions[pc];switch (get_opcode(insn)) {case GETFIELD:obj *obj = datastack[--sp];sym field_sym = get_operand(insn);datastack[sp++] = obj->get_field(field_sym);break;case SETFIELD:...}}#jjug59/127
バイトコードインタプリタ利点◼ Javaの場合、クラスファイルの内容が直接実行できる◼ スタックを自前で管理する場合、ホスト言語の制御構造ではできないことも実現できる欠点◼ コンピュータよりは遅い#jjug60/127
計算?: JITコンパイラ#jjug61/127
JITコンパイラ実行系に内蔵された翻訳系のプログラムJVMバイトコードインタプリタJavaバイトコードをコンピュータ上で実行JITコンパイラ *4Javaバイトコード機械語の命令列#jjug62/127
JITコンパイラ実行系の実行時に◼ 機械語の命令列を生成して◼ コード領域を動的確保して◼ コード領域に機械語命令列を書き込んで◼ CPUに実行させる!#jjug63/127
JITコンパイラ利点◼ 生成されたコードの実行は速い欠点◼ JITコンパイラの実行自体は重い→ よく使われる所だけを対象とする#jjug64/127
小まとめ:JVMはどのようにプログラムを動かすのか#jjug65/127
JVMはコンピュータよりも抽象度の高い枠組みを提供している◼ オブジェクトシステム◼ GC◼ 仮想関数テーブル経由の呼び出し◼ スタックフレームの保護JVMの計算はバイトコードインタプリタとJITコンパイラの組み合わせで実現される#jjug66/127
演目言語処理系の構成コンピュータはどのようにプログラムを動かすのかJVMはどのようにプログラムを動かすのかJVM言語処理系の構成:Kinkを題材に#jjug67/127
そもそもJVM言語処理系ってなに?#jjug68/127
JVM言語処理系本セッションでは◼ 実行系がJVM上で動く言語処理系、◼ または実行系がJVMそのものである言語処理系、◼ ただしJava以外のものとしておきます#jjug69/127
#jjugJVM言語処理系Scala, Kotlin, Groovy, JRuby, Jython, Clojure,Kawa, Frege, Eta, BeanShell, Pnuts, ...and Kink!70/127
なぜJVM上で処理系を作るの?#jjug71/127
#jjugGCJVMにGCが任せられるマルチスレッドの処理系で実用的に動くGCを作ることは、とてもむずかしい◼ CRubyやCPythonは、各スレッドを排他的に動かして問題を回避している(Global Interpreter Lock)◼ Luaの処理系は単一スレッドで動く72/127
#jjug移植性つくった処理系がWindowsでもLinuxでもmacOSでも動くのは嬉しい73/127
#jjug性能プログラムをJavaバイトコードに翻訳する場合、JITコンパイルのおかげで速い74/127
#jjugJava APIリッチなJava APIが使えるリフレクションのおかげで、JVM言語の側からJava APIを操作することも簡単75/127
JVM言語処理系の構成#jjug76/127
基本的な考え方JVMの性能面の利点を活かすため、JVMの仕組みをできればそのまま使いたいそのまま使えない場合は◼ JVMの仕組みとのマッピングを頑張るか、◆ invokedynamicはこれを楽にする命令◼ それも難しい場合は自前の仕組みを作る#jjug77/127
JVM言語処理系の構成の傾向ScalaKotlinGroovyJRubyKink実行系がJVMそのもの実行系に自前の要素が多い#jjug78/127
ここから先はKink処理系の実装の中で、JVMの仕組みのどの部分が◼ 使えるか◼ 使えないか◼ 使える可能性があるかを見ていきます#jjug79/127
Kinkとその処理系#jjug80/127
Kinkってこんな言語:new_dog new_val('bark' { 'Bow' }'howl' { 'Wow' })}:Dog stdout.print_line(Dog.bark) # => Bowstdout.print_line(Dog.howl) # => Wow:Another_dog Another_dog:howl stdout.print_line(Another_dog.bark) # => Bowstdout.print_line(Another_dog.howl) # => Grrr#jjugKink言語マニュアル: 特徴81/127
Kinkの処理系kinkコマンド翻訳系 実行系QcodeKinkソースコードQcode:自前のバイトコードをJavaで実装されたインタプリタで実行ひとつのコマンドで翻訳系・実行系をまかなうRubyやPythonなどと同様#jjug82/127
Kinkの処理系: 翻訳系 *5ソースコードトークン列ASTQcodeItreeWire最適化最適化木構造の中間表現フラットな中間表現#jjug83/127
Kinkの処理系: 実行系オブジェクトシステムメモリ管理計算スタック自前継承のないオブジェクトシステムJVMのGCに依存自前のバイトコードインタプリタ自前のスタック管理#jjug84/127
Kink:オブジェクトシステムとメモリ管理#jjug85/127
Kinkは継承のないオブジェクトシステムを採用しています最初のオブジェクト指向言語であるSimula Iと同様!#jjug86/127
クラスベースの継承値データ Xデータ Yクラス親クラスメソッド aメソッド bメソッド bメソッド cinstance-ofis-a例:⚫ Java⚫ Smalltalk⚫ Ruby⚫ C++#jjug87/127
プロトタイプベースの継承値データ Xデータ Yプロトタイププロトの親メソッド aメソッド bメソッド bメソッド cdelegate-todelegate-to例:⚫ JavaScript⚫ SELF⚫ Io※ Luaは変わり種#jjug88/127
delegate-to関係、必要?半端な値としての「プロトタイプ」の存在が気持ち悪い言語仕様に現れるべきものじゃなくない?処理系の実装が裏で頑張ればよくない?#jjug89/127
Kinkのオブジェクトシステム値データ Xデータ Yメソッド aメソッド bメソッド c#jjug90/127
#jjug値をつくる単にnew_val関数を呼び出して値を作る# 色new_val('R' 64'G' 128'B' 255)91/127
#jjugトレイト複数の値で共有するフィールドのかたまりをトレイトと呼ぶ(実体はただの配列)# 色のトレイト:Color_trait 'lightness' {[:C](C._max + C._min) // 2}'_max' {[:C][C.R C.G C.B].fold(0 COMPARE$max)}'_min' {[:C][C.R C.G C.B].fold(255 COMPARE$min)}]92/127
#jjugトレイトを使うトレイトはnew_val関数の引数として展開して使うnew_val(# 色のトレイトを展開... Color_trait'R' 64'G' 128'B' 255)93/127
意味は下記と同じ#jjugnew_val(# トレイトが展開された結果'lightness' { 省略 }'_max' { 省略 }'_min' { 省略 }'R' 64'G' 128'B' 255)このとおり実装すると無駄が多いので、そこは実行系が工夫する94/127
どう実装する?Javaのクラスベースのオブジェクトシステムへのマッピングはしんどい→ 自前実装#jjug95/127
オブジェクトシステムの実装Kinkの値: ValクラスcommonVarMappingtraitVarMappingownVarMapping全ての値に共通のフィールド群(不変):k1→v1, k2→v2,,,複数の値に共有されるフィールド群(不変):k1→v1, k2→v2,,,値固有のフィールド群(可変):k1→v1, k2→v2,,,#jjug96/127
メモリ管理Kinkの値: ValクラスcommonVarMappingtraitVarMappingownVarMappingk1→v1, k2→v2,,,k1→v1, k2→v2,,,k1→v1, k2→v2,,,値同士の参照関係はJavaオブジェクトの参照関係にマッピングされている→ メモリ管理はJavaのGCに丸投げできる#jjug97/127
Kink:計算とスタック管理#jjug98/127
Kinkは限定継続を使って例外やコルーチンを実現しています#jjug99/127
限定継続とはスタックを切り貼りする仕組みKinkではreset関数とshift関数の組み合わせで限定継続を操作#jjug100/127
reset & shift: 継続の保存reset: スタック上にタグを打つtagresetブロック内の処理tagshift: タグまでの切片を関数(=継続)に保存tagtagkont =#jjug101/127
継続は呼び出せるkont(Val): 継続の呼び出し:継続に保存されたスタック切片を貼り付けるよその処理tag#jjug102/127
スタックをどう実装する?JVMのスタックをそのまま使うのは難しい◼ JVMにはスタックをプログラムから操作する方法がない※ でも世の中には無茶する人がいる※ Quasarライブラリは例外とバイトコード変換で無理矢理操作※ Project Loomで状況が変わるかも→ Kinkは自前のスタックを実装#jjug103/127
計算をどう実装する?KinkプログラムをJavaバイトコードに翻訳するのは苦労が多そう◼ 自前のオブジェクトシステム◆ putfield/getfield命令が使えない◼ 自前の呼び出しスタック◆ invoke*命令が使えない→ 自前のバイトコードインタプリタ#jjug104/127
Kink:性能#jjug105/127
ベンチマーク: たらいまわし関数#jjug:tarai (X <= Y).if_else({ Y }{ tarai(tarai(X - 1 Y Z)tarai(Y - 1 Z X)tarai(Z - 1 X Y))})}tarai(13 6 0)106/127
#jjugtarai(13 6 0)3.8 秒7.7 秒12.0 秒30.3 秒43.5 秒Ruby2.5.1Python3.6.7GNU awk4.1.4GNU bc1.07.1Kink@ Xeon E5-2699 v4107/127
プロファイリング#jjug紫色の部分がスタック操作バイトコードインタプリタと合わせて、4割~を占める→ 自前実行系の限界?108/127
性能改善の余地計算をJVMにプッシュバックすれば性能改善が期待できる、かも◼ JRuby方式:◆ 自前バイトコードからJavaバイトコードへのJITコンパイル◆ 最も実行される部分は、さらにJVMが機械語にJITコンパイルする◼ Graal+Truffleに期待できる?#jjug109/127
まとめ#jjug110/127
JVM言語処理系の構成にはさまざまなバリエーションがあるJVMの仕組みがそのまま使えると嬉しい◼ GC: 自前で作るのは大変!◼ JITコンパイラ: 計算が速くなるJVMの仕組みへのマッピングはときに大変#jjug111/127
演目言語処理系の構成コンピュータはどのようにプログラムを動かすのかJVMはどのようにプログラムを動かすのかJVM言語処理系の構成:Kinkを題材に#jjug112/127
参考文献#jjug113/127
中田育夫『コンパイラ: つくりながら学ぶ』, 2017◼ コンパイラと実行系をつくる◼ 易しくて良い本#jjug114/127
Abelson, Sussman, Sussman『Structure and Interpretation of ComputerPrograms』, 1996◼ いわゆる「SICP」◼ Scheme言語の上で、Scheme言語の実行系を作ろう!という本◼ 5章がバイトコードインタプリタ◆ 自前のメモリ管理も扱われています#jjug115/127
Patterson, Hennessy『Computer Organization and Design: RISC-VEdition』, 2017◼ いわゆる「パタヘネ」◆ 「ヘネパタ」は別書なので注意!◼ CPUがプログラムをどうやって動かすか、という本#jjug116/127
Pierce『型システム入門』, 訳書2013◼ いわゆる「TAPL」◼ 型システムの教科書◼ 序盤の3章~7章が特にオススメ◆ 型を導入する準備として、型のない項書換え系が解説されている◆ 型なしラムダ計算はこれでバッチリ!#jjug117/127
中田育夫『コンパイラの構成と最適化』, 2版, 2009◼ 主に最適化の本◼ むずかしい#jjug118/127
浅井健一「shift/resetプログラミング入門」, 2011◼ 限定継続を紹介する文書#jjug119/127
Gasbichler, Sperber「Final Shift for call/cc: DirectImplementation of Shift and Reset」, 2002◼ Scheme処理系に限定継続を実装したよ、という論文◼ Kinkにおける限定継続の実装は、3節の「Direct Implementation」に該当する#jjug120/127
Chambers, Ungar, Lee「An Efficient Implementation of SELF」,1991◼ SELF言語の実行系をどうやって高速化したか◼ ChromeのJSエンジンであるV8でも参考にされています: Fast Property Access#jjug121/127
脚注#jjug122/127
*1 翻訳系、実行系次の分け方が普通(中田 (2017))◼ 変換系 = Translator◼ 翻訳系 = Compiler◼ 通訳系 = Interpreter変換は翻訳の亜種/一部と割り切りました「通訳」は分かりづらい、「インタプリタ」はソースを直接動かすものと誤解されがち、ということで「実行系」としました#jjug123/127
*2 プログラムをロード実際は、プログラムの実行ファイルを、プロセスの仮想アドレス空間にメモリマップします◼ @akachochin「Linuxのユーザプロセスのメモリマップについて」, 2017#jjug124/127
*3 CPUの概念図Patterson, Hennessy (2017) のFigure 4.11を参考にしました#jjug125/127
*4 JITコンパイラGraalの場合、JITコンパイラの入力はJavaバイトコードに限りません#jjug126/127
*5 Kinkの翻訳系ソース: FunHelper#compile#jjug127/127