Mercurial > hg > Events > OSC2019
view slide.md @ 18:1fc9d0bd924f default tip
update
author | anatofuz <anatofuz@cr.ie.u-ryukyu.ac.jp> |
---|---|
date | Sat, 20 Apr 2019 18:06:35 +0900 |
parents | a176ea5c0264 |
children |
line wrap: on
line source
title: Perl6の内部表現 author: Takahiro Shimizu profile: lang: Japanese ## このセッションの内容 - Perl6の主要な実装であるRakudoの内部構造を探ります - Rakudoの内部で利用されているVMや, Perl6のサブセットなどについて探索します - スクリプト言語で主に使われているバイトコードインタプリタの気持ちになります ## 内容 - Perl6とは? - スクリプト言語処理系の動き - Perl6の内部構造 - NQP - MoarVM - NQPとMoarVMのバイトコード対応 - バイトコードインタプリタのC言語実装 - MoarVMの詳細 - MoarVMのバイトコード実行 - まとめ ## Perl6とは - 当初Perl5の時期バージョンとして開発されていたプログラミング言語 - 現在は別の言語として開発がそれぞれ進んでいる - 仕様と実装が分離しており, 現在はテストが仕様となっている - 実装は歴史上複数存在しているが,主流な実装はRakudo - Haskellで実装されたPugs - Pythonとの共同基板を目指したParrot - 言語的にはスクリプト言語であり, 漸進的型付き言語 - 動作環境は、独自のVMのMoarVM, JVM、一部JavaScript上で動作する <img src="fig/2000px-Camelia.svg.png" alt="" style="width: 31%; height: auto;"> ## 現在のPerl6 - 現在のバージョンは `6.d` - [ブラウザ上で実行可能な環境](https://perl6.github.io/6pad/)が存在する - [IDE](https://commaide.com/)が開発されている - WebApplicationFrameworkなども開発されており、 Perl5のモジュールを移行したものがいくつか存在する - 日本では趣味のプロダクト以外社会では使用されていない - 海外では実際に使われているケースも存在する - 処理速度では一部Perl5に勝っているが、それでも大分遅い ## [参考]Perl5のソースコード - Perl5時代 − スカラ、配列、ハッシュの3種類 - それぞれの変数への参照であるリファレンスが使用可能 ```perl use ustrict; use warnings; my $scalar_value = "hello!"; print "$scalar_value\n"; my @array = (1..10); print "$array[0]\n"; my %hash = ( this_is_key => "this_is_value"); print "$hash{this_is_key}\n"; my $hash_ref = \%hash; print "$hash_ref->{this_is_key}\n"; ``` ## Perl6のソースコード概要 - Perl5の文法とは比較的変更が多い - 雰囲気は似ている - 変数がオブジェクトと化した事により, 変数からsayメソッドを呼ぶことが可能 ``` my $str_value = 'hello world!'; $str_value.say; # hello world! ``` - Perl5と同様に,変数にはデフォルトでは型がないような振る舞いをする ``` my $sample_value = 'hello world!'; $sample_value.say; # hello world! $sample_value = '31'; $sample_value.say; # 31 say($sample_value * 3); ``` ## Perl6の言語的な特徴 - 漸進的型付き言語である為, 型を強制することも可能となる ``` my Int $int_value = 31; $int_value = "hello"; # Compile error! ``` ``` $ perl6 type_invalid.p6 Type check failed in assignment to $int_value; expected Int but got Str ("hello") in block <unit> at type_invalid.p6 line 4 ``` ## Perl6の言語的な特徴 - 型を独自に定義することも可能 - 入力の型によって実行する関数を変える事などができる ```perl6 my subset Fizz of Int where * %% 3; my subset Buzz of Int where * %% 5; my subset FizzBuzz of Int where Fizz&Buzz; my subset Number of Int where none Fizz|Buzz; proto sub fizzbuzz ($) { * } multi sub fizzbuzz (FizzBuzz) { "FuzzBuzz" } multi sub fizzbuzz (Fizz) { "Fizz" } multi sub fizzbuzz (Buzz) { "Buzz" } multi sub fizzbuzz (Number $number) { $number } fizzbuzz($_).say for 1..15; ``` - 型を利用したFizzBuzz ## スクリプト言語 - Perl6は現状コンパイルすることはできない - スクリプト言語の分類 - 現在広く使われているスクリプト言語(Perl,Python,Ruby...)などとPerl6の構成は類似している - 今回はPerl6の実装を追いながら、最近のスクリプト言語処理系の大まかな実装を理解する ## スクリプト言語処理系 - スクリプト言語は入力として与えられたソースコードを、 直接評価せずにバイトコードにコンパイルする形式が主流となっている - その為スクリプト言語の実装は大きく2つで構成されている - バイトコードに変換するフロントエンド部分 - バイトコードを解釈する仮想機械 <img src="fig/bytecode_sample_generally_lang.svg" width="80%"> ## Perl6以外のスクリプト言語 - 現在使われているプロセスVMは言語に組み込まれているものが多い - JVMやElixirなどのVMは複数の言語で使用されている - Java - JVM - Ruby - YARV - Python - PythonVM - Erlang - Elixir - BEAM ## Perl6の処理系の構成 - Perl6の処理系で現在主流なものはRakudoと呼ばれる実装である(歴史上複数存在する) - Rakudoは3つのレイヤーから構成されている - Perl6インタプリタ - Perl6インタプリタを記述するPerl6のサブセットNQP - Perl6のバイトコードを解釈するMoarVM - Perl6/NQPがフロントエンドに相当し、MoarVMがバックエンドに相当する ## Rakudoの構成図 ![](fig/Rakudo_overview.svg) (http://brrt-to-the-future.blogspot.com/2015/03/advancing-jit-compiler.html) ## Perl6とNQP - NQP(NotQuitPerl Perl) - Perl6のサブセット。Perl6っぽい言語 - Perl6、 NQP自体がNQPで記述されている - NQPもNQPで記述されている為、 セルフビルド(自分自身で自分自身をコンパイルする)を行う - NQPはPerl6の文法をベースにしているが、 制約がいくつか存在する - 元々はPerl6の主力実装がParrotだった時代に登場 - 文法がアップデートされており、当時の資料は古くなっている ``` my $value := "hello!"; say($value); ``` ## NQPスクリプト - 変数は束縛 `:=` を使う − 関数の間に空白を入れてはいけない - 再帰呼び出しを使うフィボナッチ数列 ``` #! nqp sub fib($n) { $n < 2 ?? $n !! fib($n-1) + fib($n - 2); } my $N := 29; my $z := fib($N); nqp::say("fib($N) = " ~ fib($N)); ``` ## NQPスクリプト(nまでの整数の和) ```perl6 sub add_test($n){ my $sum := 0; while ( $n > 1) { $sum := $sum + $n; --$n; } return $sum; } say(add_test(10000)); ``` ## NQP - NQPはPerl6の中で一番レイヤーが低い言語 - その為、 実行するVMのオペコード(処理単位)を使用することができる - NQPオペコードは、 Perl6の内部の抽象構文木でも使用されている - また、 Perl6と同様に型を指定することが可能 ```perl6 sub add_test(int $n){ mu $sum := 0; while nqp::isgt_i($n,1) { $sum := nqp::add_i($sum,$n); $n := nqp::sub_i($n,1); } return $sum; } ``` ## NQPとMoarVM - NQPは実行する際にMoarVM/JVMが必要となる - NQPコンパイラが各VMに対応したバイトコードに変換する - MoarVMの場合は、MoarVMのバイナリ moar に、 NQPのインタプリタのバイトコードをライブラリや入力として与える ## Perl6のVM - MoarVM, JVM , JavaScriptが選択可能 - メインで開発されているのはMoarVMであり、 他のVMは機能が実装されていないものが存在する - `rakudo-star` というPerl6のパッケージ環境では、 MoarVMがデフォルトでインストールされる ## MoarVM - Metamodel On A Runtime - C言語で記述されているPerl6専用の仮想機械 - レジスタマシン - 型情報を持つレジスタに対しての演算として処理される - Rubyなどはスタックマシンとして実装されている - Unicodeのサポートや、LuaJITなどを利用したJITコンパイルなども可能 - Perl6やNQPは、MoarVMに対してライブラリなどを設定して起動する ## バイトコード - Perl6も、Rakudo/NQPはバイトコードに変換され、 バイトコードをVMが実行する - Perl6/NQPはバイトコードにコンパイルすることが可能 - 直接実行することはできない ``` $nqp --target=mbc --output=fib.moarvm fib.nqp ``` ## バイトコード - バイナリ形式で表現される為、 VMがどのように読み取るかでバイトコードの意味が異なる - スクリプト言語系のVMは、 VMという名前の通り、 計算機をエミュレートしている - その為、通常のCPUのストア命令などに相当する命令が実装されている - スクリプト言語は、その命令の実行を繰り返すことでプログラムを評価する - スクリプト言語で重要なバイトコード表現は、「仮想機械がどの命令を実行するか」のバイトコード - CPUに対するアセンブラの数値に対応する - どういった構成なのかは仮想機械によって異なる ## バイトコードとMoarVM - MoarVMバイトコードはMoarVMの実行バイナリ `moar` でディスアセンブルすることが可能 ``` annotation: add_test.nqp:1 00003 const_i64_16 loc_2_int, 0 00004 hllboxtype_i loc_3_obj 00005 box_i loc_3_obj, loc_2_int, loc_3_obj 00006 set loc_1_obj, loc_3_obj label_1: 00007 decont loc_3_obj, loc_0_obj 00008 smrt_numify loc_4_num, loc_3_obj 00009 const_i64_16 loc_2_int, 1 00010 coerce_in loc_5_num, loc_2_int 00011 gt_n loc_2_int, loc_4_num, loc_5_num 00012 unless_i loc_2_int, label_2(00031) 00013 osrpoint annotation: add_test.nqp:3 00014 decont loc_3_obj, loc_1_obj 00015 smrt_numify loc_5_num, loc_3_obj 00016 decont loc_3_obj, loc_0_obj 00017 smrt_numify loc_4_num, loc_3_obj 00018 add_n loc_4_num, loc_5_num, loc_4_num 00019 hllboxtype_n loc_3_obj 00020 box_n loc_3_obj, loc_4_num, loc_3_obj 00021 set loc_1_obj, loc_3_obj 00022 decont loc_3_obj, loc_0_obj 00023 smrt_numify loc_4_num, loc_3_obj 00024 coerce_ni loc_6_int, loc_4_num 00025 const_i64_16 loc_7_int, 1 00026 sub_i loc_7_int, loc_6_int, loc_7_int 00027 hllboxtype_i loc_3_obj 00028 box_i loc_3_obj, loc_7_int, loc_3_obj 00029 set loc_0_obj, loc_3_obj 00030 goto label_1(00007) ``` ## NQPとバイトコードの対応 ``` say(add_test(10000)); ``` ``` annotation: add_test.nqp:1 label_1: 00020 getlex_no loc_7_obj, '&say' 00021 decont loc_7_obj, loc_7_obj 00022 const_s loc_3_str, '&add_test' 00023 getlexstatic_o loc_8_obj, loc_3_str 00024 decont loc_8_obj, loc_8_obj 00025 const_i64_16 loc_5_int, 10000 00026 prepargs Callsite_1 00027 arg_i 0, loc_5_int 00028 invoke_o loc_8_obj, loc_8_obj 00029 prepargs Callsite_0 00030 arg_o 0, loc_8_obj 00031 invoke_v loc_7_obj 00032 null loc_7_obj 00033 return_o loc_7_obj ``` - Perl6の変数は直接実態を参照せず、中身が入っているコンテナを参照するようになっている。 - その為 `decont` 命令で、コンテナの中身をレジスタに設定する必要がある - `const_i64_16` などは64bitの数という意味で、 `int` 型としてレジスタに登録している - `prepargs` で引数の確認を行い, `invoke_o` で実際にサブルーチンに移行する ## NQPとバイトコードの対応 ``` my $sum := 0; ``` ``` annotation: add_test.nqp:1 00003 const_i64_16 loc_2_int, 0 00004 hllboxtype_i loc_3_obj 00005 box_i loc_3_obj, loc_2_int, loc_3_obj 00006 set loc_1_obj, loc_3_obj ``` - まず `loc_2` レジスタをint型の整数0で初期化する - 変数 `$sum` はint型の指定がないので、 obj型で登録しなければならない - その為, 整数として登録された `loc_2` から、 obj型に一旦キャストし、 `loc_3` レジスタに設定したものを、 `loc_1` レジスタに設定する ## NQPとバイトコードの対応 ``` while ( $n > 1) { ``` ``` label_1: 00007 decont loc_3_obj, loc_0_obj 00008 smrt_numify loc_4_num, loc_3_obj 00009 const_i64_16 loc_2_int, 1 00010 coerce_in loc_5_num, loc_2_int 00011 gt_n loc_2_int, loc_4_num, loc_5_num 00012 unless_i loc_2_int, label_2(00031) 00013 osrpoint ``` − 比較にもint型の指定がない為、 `num` 型にキャストし、 `num` 型のレジスタでの大小を比較する - 比較命令は `gt_n` であり、 結果により `unless_i` 命令で、別のラベルにジャンプする ## decode命令 ``` while ( $n > 1) { ``` ``` 00007 decont loc_3_obj, loc_0_obj ``` ![](fig/decont_perl6_loc3.svg) - 変数 `$n` と 整数 `1` を大小比較する為、 まず `$n` から値を取り出す - とりだした時点では、何の型で使うかは決定していない為、 obj型として判定する ## smrt_nomify ``` while ( $n > 1) { ``` ``` 00008 smrt_numify loc_4_num, loc_3_obj ``` - `smrt_numify` はレジスタ上のオブジェクトを、 num型に変換し、 別のレジスタに登録する命令 - 今回の整数の比較では、 int型の強制がない為、 数値として比較するためにn64型にキャストしている - numはn64型であり、 64ビットの浮動小数点定数の意味 ![](fig/perl6_num_convert.svg) ## const_i64_16とcoerece_in ``` while ( $n > 1) { ``` ``` 00009 const_i64_16 loc_2_int, 1 00010 coerce_in loc_5_num, loc_2_int ``` - 64ビット整数として1をレジスタ `loc_2` に設定する - その後、 numでの比較のためにキャストし, `loc_5` レジスタに設定している ## 比較とif文の判定 ``` while ( $n > 1) { ``` ``` 00011 gt_n loc_2_int, loc_4_num, loc_5_num 00012 unless_i loc_2_int, label_2(00031) ``` - `gt_n` で `loc_4` (nが入っている) レジスタと `loc_5` (1が入っているレジスタ)を比較する - 結果により `loc_2` レジスタに真偽値がint型で代入される - その結果を `unless_i` で読み取り、 値が偽であったら `label_2(00031)` にジャンプする ## C言語での実装へ - 今まではスクリプトレベルでの実装を見てきました − スクリプトが実行されるVMの実装をCレベルで見ていきましょう ## MoarVMのバイトコードインタプリタ部分 MoarVMなどの言語処理系のバイトコードインタプリタは次のことを繰り返している 1. 入力されたバイトコード列から命令に対応する部分を読み取る 2. 読み込んだ数値から、 対応する命令を取得する 3. 命令部分を実行する 4. バイトコード列を次に進め、繰り返す - この部分の実装は大体次のようなパターンで記述されている ## 巨大なswitch文を使うケース - 命令に対応するバイトコードを数値に変換できるようにし、 switch-case文で分岐させる - 実行のたびにループで先頭に戻り、次の命令を計算する必要があるので低速 ``` while( pc != NULL) { switch(pc){ case ADD_INSTRUCTION: // instruction.... break; case SUBD_INSTRUCTION: // instruction.... break; } } ``` ## Cコンパイラのラベルgotoを使うケース - 巨大なcase文とループではなく、 次の命令の実行場所に直接jmpで移動する - GCC拡張でサポートしている場合「`&&ラベル名` 」でラベルのアドレスが取得できる - 取得したアドレスに対して「goto アドレス」でgoto文でjmpする - 次の命令に対応するラベルを取得する必要があるが、 ループする必要がなく高速 - ラベルgotoであり、 Cコンパイラの拡張機能として搭載されている - gccおよびLLVM/clangには実装されている ``` static const void *CODES[] = {&&ADD_INSTRUCTION, &&SUB_INSTRCUTION}; goto *CODES[pc]; ADD_INSTRUCTION: // instruction... pc++; goto *CODES[pc]; SUB_INSTRUCTION: // instruction... pc++; goto *CODES[pc]; ``` ## MoarVMでは - ラベルgotoが利用できる場合は利用する - 使えないコンパイラの場合は、 switch文を利用する - この判断はマクロで処理をしている − 一般的にはラベルgotoの方が高速である為、他のスクリプト言語でもラベルgotoが使われている ## MoarVMのC言語での実装 - [GitHub上にリポジトリ](https://github.com/MoarVM/MoarVM)がある - `src/core/interp.c` がバイトコードインタプリタの実装コード - この中でマクロを使いつつ、 バイトコードに対応した命令を処理している - `MVM_interp_run`が実際にバイトコードを解釈している ``` /* This is the interpreter run loop. We have one of these per thread. */ void MVM_interp_run(MVMThreadContext *tc, void (*initial_invoke)(MVMThreadContext *, void *), void *invoke_data) { #if MVM_CGOTO #include "oplabels.h" #endif /* Points to the place in the bytecode right after the current opcode. */ /* See the NEXT_OP macro for making sense of this */ MVMuint8 *cur_op = NULL; /* The current frame's bytecode start. */ MVMuint8 *bytecode_start = NULL; /* Points to the base of the current register set for the frame we * are presently in. */ MVMRegister *reg_base = NULL; /* Points to the current compilation unit. */ MVMCompUnit *cu = NULL; /* The current call site we're constructing. */ MVMCallsite *cur_callsite = NULL; /* Stash addresses of current op, register base and SC deref base * in the TC; this will be used by anything that needs to switch * the current place we're interpreting. */ tc->interp_cur_op = &cur_op; tc->interp_bytecode_start = &bytecode_start; tc->interp_reg_base = ®_base; tc->interp_cu = &cu; /* With everything set up, do the initial invocation (exactly what this does * varies depending on if this is starting a new thread or is the top-level * program entry point). */ initial_invoke(tc, invoke_data); ``` ## MoarVMのレジスタ構成 - レジスタはそれぞれの型を共用体のデータ構造で持っている - 型名は各命令の接尾辞に対応している ``` /* Different views of a register. */ union MVMRegister { MVMObject *o; MVMString *s; MVMint8 i8; MVMuint8 u8; MVMint16 i16; MVMuint16 u16; MVMint32 i32; MVMuint32 u32; MVMint64 i64; MVMuint64 u64; MVMnum32 n32; MVMnum64 n64; }; ``` ## MVM_interp_runの登場人物 - `cur_op` が読み取るのバイトコード列の先頭ポインタを保存している - `op` が現在実行する命令の数値が入っている - `reg_base` がMoarVMのレジスタの集合配列 ``` /* Points to the place in the bytecode right after the current opcode. */ /* See the NEXT_OP macro for making sense of this */ MVMuint8 *cur_op = NULL; /* The current frame's bytecode start. */ MVMuint8 *bytecode_start = NULL; /* Points to the base of the current register set for the frame we * are presently in. */ MVMRegister *reg_base = NULL; ``` ## MVM_interp_runメインループ - `runloop`がメインのループで処理を行っている - `DISPATCH`部分で命令をバイトコード列から取り出している ``` /* Enter runloop. */ runloop: { MVMuint16 op; #if MVM_TRACING if (tracing_enabled) { char *trace_line; trace_line = MVM_exception_backtrace_line(tc, tc->cur_frame, 0, cur_op); fprintf(stderr, "Op %d%s\n", (int)*((MVMuint16 *)cur_op), trace_line); /* slow tracing is slow. Feel free to speed it. */ MVM_free(trace_line); } #endif /* The ops should be in the same order here as in the oplist file, so * the compiler can can optimise the switch properly. To check if they * are in the same order as the oplist use the * tools/compare-oplist-interp-order.sh helper script. */ DISPATCH(NEXT_OP) { OP(no_op): goto NEXT; OP(const_i8): OP(const_i16): OP(const_i32): MVM_exception_throw_adhoc(tc, "const_iX NYI"); OP(const_i64): GET_REG(cur_op, 0).i64 = MVM_BC_get_I64(cur_op, 2); cur_op += 10; goto NEXT; ``` ## MVM_interp_runメインループ - `DISPATCH`はマクロになっている - ラベルgotoが使えるケースはDISPATCHは何も意味を持たない - 使えない場合はswitch文に展開される - `op` に実行する命令の数値が格納されているので、 これに応じてswitchで飛ぶ ``` #if MVM_CGOTO #define DISPATCH(op) #define OP(name) OP_ ## name #define NEXT *LABELS[NEXT_OP] #else #define DISPATCH(op) switch (op) #define OP(name) case MVM_OP_ ## name #define NEXT runloop #endif ``` ## MVM_interp_runメインループ - `OP` が命令のスコープを切るマクロとなっている - gotoが使える場合 - `##name` に展開され、 それぞれの命令の名前を持つラベルになる - gotoが使えない場合 - case文に展開される ``` #if MVM_CGOTO #define DISPATCH(op) #define OP(name) OP_ ## name #define NEXT *LABELS[NEXT_OP] #else #define DISPATCH(op) switch (op) #define OP(name) case MVM_OP_ ## name #define NEXT runloop #endif ``` ## MVM_interp_runメインループ - 次の命令には `NEXT` マクロで移動する - gotoが使える場合 - 次のラベルに対応したアドレスを配列LABELSから取り出しgotoする - gotoが使えない場合 - `runloop`に対してgotoする(先頭へのループ) ``` #if MVM_CGOTO #define DISPATCH(op) #define OP(name) OP_ ## name #define NEXT *LABELS[NEXT_OP] #else #define DISPATCH(op) switch (op) #define OP(name) case MVM_OP_ ## name #define NEXT runloop #endif ``` ## それぞれの命令の実装 - マクロ `GET_REG` でレジスタ情報を取得する - 末尾で型を指定する - よしなに処理をした後に、バイトコード列 `cur_op` をインクリメントする - 計算機のプログラムカウンタを進めることに対応する ``` OP(add_i): GET_REG(cur_op, 0).i64 = GET_REG(cur_op, 2).i64 + GET_REG(cur_op, 4).i64; cur_op += 6; goto NEXT; OP(sub_i): GET_REG(cur_op, 0).i64 = GET_REG(cur_op, 2).i64 - GET_REG(cur_op, 4).i64; cur_op += 6; goto NEXT; OP(mul_i): GET_REG(cur_op, 0).i64 = GET_REG(cur_op, 2).i64 * GET_REG(cur_op, 4).i64; cur_op += 6; goto NEXT; ``` ## 本日の展示について - 現在当研究室で開発しているContinuationBasedC(CbC)で一部Perl6の処理系を書いています - また、 CbCを用いたGearsOSの展示も行っています ## まとめ - Perl6の内部構造に迫りました - スクリプト言語の中身も通常の計算機と同じような処理をしています - OSSなので皆さん読んで改造してみましょう!!