view Slide/slide.md @ 88:632f160ccbd0

update
author Takahiro SHIMIZU <anatofuz@cr.ie.u-ryukyu.ac.jp>
date Thu, 10 Jan 2019 21:55:46 +0900
parents 2c38abf2c77d
children 1f9baa69dfe0
line wrap: on
line source

title: CbCによるPerl6処理系
author: Takahiro Shimizu, Shinji Kono
profile: 琉球大学
lang: Japanese
code-engine: coderay


## 研究目的
-  スクリプト言語であるPerl5の後継言語としてPerl6が現在開発されている.
-  現在主流なPerl6の実装にRakudoがあり, RakudoはNQP(Perl6のサブセット)で記述されたPerl6, NQPで記述されたNQPコンパイラがある
-  NQPコンパイラはRakudoのVMであるMoarVM用のバイトコードを生成し, MoarVMはこのバイトコードを解釈, 実行する
-  Continuation based C (CbC)という言語は継続を基本とするC言語であり, 言語処理系に応用出来ると考えられる
-  CbC一部用いてMoarVMの書き換えを行い, 処理を検討する.

## Continuation Based C (CbC)

- Continuation Based C (CbC) はCodeGearを単位として用いたプログラミング言語である.
- CodeGearはCの通常の関数呼び出しとは異なり,スタックに値を積まず, 次のCodeGearにgoto文によって遷移する.
- このgoto文による遷移を軽量継続と呼ぶ.
- CodeGearはCの関数宣言の型名の代わりに`__code`と書く事で宣言出来る.

```
extern int printf(const char*,...);
  int main (){
     int data = 0;
     goto cg1(&data);
}
__code cg1(int *datap){
      (*datap)++;
    goto cg2(datap);
}
__code cg2(int *datap){
    (*datap)++;
    printf("%d\n",*datap);
}
```

## CbCの現在の実装

- CbCは現在3種類の実装がある.
  - gcc (version 9.0.0)
  - llvm/clang (version 7.0.0)
  - micro-c

## 言語処理系の応用
- スクリプト言語処理系は, バイトコードにコンパイルされ, バイトコードをJITを用いてネイティブに変換する
- JITを使わない場合, バイトコードに対応した, case文や, ラベルのテーブルにgotoすることで処理を実行する
- CbCを言語処理系に応用した場合, バイトコードに対応するCodeGearを生成することが可能である
- バイトコードに対応したCodeGearは, CodeGearのテーブルを経由することで実行出来る
- CodeGearに分割することで, 処理を複数の関数で記述する事が出来, ファイル分割などのモジュール化が可能となる

## Rakudo
- Rakudoとは現在のPerl6の主力な実装である.
- 実行環境のVM, Perl6のサブセットであるNQP(NotQuitPerl), NQPで記述されたPerl6(Rakudo)という構成になっている.
- コンパイラは, NQPで記述されたPerl6コンパイラ, NQPで記述されたNQPコンパイラ, MoarVMバイトコードを解釈するMoarVMという構成である

- 現在はMoarVMがRakudoの中でも主流なVM実装となっている.

## MoarVM

- Perl6専用のVMであり, Cで記述されている
- レジスタマシンとして実装されている.
- MoarVMはバイトコードインタプリタを `src/core/interp.c` で定義しており, この中の関数 `MVM_interp_run` で命令に応じた処理を実行する

## MVM_interp_run

- DISPATCHマクロは次の様に記述されており, この中の `OP` で宣言されたブロックがそれぞれオペコードに対応する処理となっている.
- この中では `GET_REG` などのマクロを用いてMoarVMのレジスタにアクセスする.
- `cur_op`は次のオペコード列が登録されており, マクロ `NEXT` で決められた方法で次のオペコードに遷移する.

```
DISPATCH(NEXT_OP) {
    OP(const_i64):
        GET_REG(cur_op, 0).i64 = MVM_BC_get_I64(cur_op, 2);
        cur_op += 10;
        goto NEXT;
}

```

## MVM_interp_run

- MVM_interp_runでは次のオペコードをフェッチする際に `NEXT_OP` マクロを介して計算を行う.
- オペコードが対応する命令を実行する際は, `MVM_CGOTO` フラグが立っている場合はCのラベルgotoを利用し, 使えない場合はswitch文を利用して遷移する.


```
#define NEXT_OP (op = *(MVMuint16 *)(cur_op), cur_op += 2, op)

#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

- ラベル遷移を利用する場合は配列`LABELS`にアクセスし, ラベル情報を取得する

```
static const void * const LABELS[] = {
    &&OP_no_op,
    &&OP_const_i8,
    &&OP_const_i16,
    &&OP_const_i32,
    &&OP_const_i64,
    &&OP_const_n32,
    &&OP_const_n64,
    &&OP_const_s,
    &&OP_set,
    &&OP_extend_u8,
    &&OP_extend_u16,
    &&OP_extend_u32,
    &&OP_extend_i8,
    &&OP_extend_i16,
```


## MVM_interp_run

- Cの実装の場合, switch文に展開される可能性がある為, 命令ディスパッチが書かれているCソース・ファイルの指定の場所にのみ処理を記述せざるを得ない
    - その為, 1ファイルあたりの記述量が膨大になり, 命令のモジュール化ができない
- Threaded Codeの実装を考えた場合, この命令に対応して大幅に処理系の実装を変更する必要がある.
- デバッグ時には今どの命令を実行しているか, ラベルテーブルを利用して参照せざるを得ず, 手間がかかる.



## CbCMoarVMのバイトコードディスパッチ

- interp.cではマクロを利用した cur_op (現在のオペコード) の計算及び, マクロ遷移かswitch文を利用して次の命令列に遷移していた
- CbCMoarVMでは, それぞれの命令に対応するCodeGearを生成し, このCodeGearの集合であるテーブルCODESを作成した
- このテーブルは`cbc_next`というCodeGearから参照し, 以降はこのCodeGearの遷移として処理が継続される.

```
#define NEXT_OP(i) (i->op = *(MVMuint16 *)(i
    ->cur_op), i->cur_op += 2, i->op)
#define DISPATCH(op) {goto (CODES[op])(i);}
#define OP(name) OP_ ## name
#define NEXT(i) CODES[NEXT_OP(i)](i)
static int tracing_enabled = 0;

_code cbc_next(INTERP i){
    goto NEXT(i);
}
```

```
__code (* CODES[])(INTERP) = {
  cbc_no_op,
  cbc_const_i8,
  cbc_const_i16,
  cbc_const_i32,
  cbc_const_i64,
  cbc_const_n32,
  cbc_const_n64,
  cbc_const_s,
  cbc_set,
  cbc_extend_u8,
  cbc_extend_u16,
```

## CodeGearの入出力インターフェイス

- MoarVMではレジスタの集合や命令列などをMVM_interp_runのローカル変数として利用し, 各命令実行箇所で参照している
- CodeGearに書き換えた場合, このローカル変数にはアクセスする事が不可能となる.
- その為, 入出力としてMoarVMの情報をまとめた構造体interpのポインタであるINTERPを受け渡し, これを利用してアクセスする


```
typedef struct interp {
    MVMuint16 op;
    MVMuint8 *cur_op;
    MVMuint8 *bytecode_start;
    MVMRegister *reg_base;
     /* Points to the current compilation unit
         . */
    MVMCompUnit *cu;
     /* The current call site we’re
         constructing. */
    MVMCallsite *cur_callsite;
    MVMThreadContext *tc;
 } INTER,*INTERP;
```

## DataGearへの変換

- バイトコードに対応する命令をそれぞれCodeGearに変換していく.
- `OP(.*)`の`(.*)`の部分をCodeGearの名前として先頭に `cbc_` をつけた上で設定する.
- cur_opなどはINTERPを経由してアクセスする様に修正する.
- 末尾の `NEXT` を次のCodeGearにアクセスする為に `cbc_next` に修正する.
- case文で次のcase文に流れる箇所は, 直接その下のcase文に該当するCodeGearに遷移する.
- GC対策の為に, CodeGear中のローカル変数をグローバル変数の配列に保存している箇所は,CodeGearに直接処理を書かず, CodeGearから別の関数を呼び出す形に修正する
    - その際に, 保存するローカル変数をstatic変数に修正するなどの工夫を行う


```
__code cbc_no_op(INTERP i){
    goto cbc_next(i);
}
__code cbc_const_i8(INTERP i){
    goto cbc_const_i16(i);
}
__code cbc_const_i16(INTERP i){
    goto cbc_const_i32(i);
}
__code cbc_const_i32(INTERP i){
    MVM_exception_throw_adhoc(i->tc, "const_iX NYI");
   goto cbc_const_i64(i);
}
__code cbc_const_i64(INTERP i){
    GET_REG(i->cur_op, 0,i).i64 = MVM_BC_get_I64(i->cur_op, 2);
    i->cur_op += 10;
    goto cbc_next(i);
}
__code cbc_pushcompsc(INTERP i){
    MVMObject * sc;
    sc  = GET_REG(i->cur_op, 0,i).o;
    if (REPR(sc)->ID != MVM_REPR_ID_SCRef)
        MVM_exception_throw_adhoc(i->tc, "Can only push an SCRef with pushcompsc");
    if (MVM_is_null(i->tc, i->tc->compiling_scs)) {
        MVMROOT(i->tc, sc, {
            i->tc->compiling_scs = MVM_repr_alloc_init(i->tc, i->tc->instance->boot_types.BOOTArray);
        });
    }
    MVM_repr_unshift_o(i->tc, i->tc->compiling_scs, sc);
    i->cur_op += 2;
    goto cbc_next(i);
}
```

## NQP
- Perl6の機能を制約したプログラミング言語であり, Perl6はNQPで記述されている
    - その為Perl6処理系は, NQPの動作を目的に実装することでPerl6の動作が可能となる
    - NQPコンパイラ自身もNQPで記述されている
- Perl6と違い, 変数の宣言を `:=` を利用した束縛で行う, `++` 演算子が使用できないなどの違いがある
- nqpのオペコードを利用する際に,型を指定する事が可能である

```
sub add_test(int $n) {
    my $sum := 0;
    while nqp::isgt_i($n,1) {
        $sum := nqp::add_i($sum,$n);
        $n := nqp::sub_i($n,1);
    }
    return $sum;
}

say(add_test(10));
```

## MoarVMのデバッグ手法

- MoarVMはバイトコードをランダムに生成する仕様となっている
    - 一旦moarvmバイトコードとして出力したファイルを実行する場合は同じ処理内容となっている
- そのため, MoarVMのデバッグは同じバイトコードを入力として与え, オリジナルのMoarVMと並列してgdbを用いてトレースを行う.
- この際, 実行するバイトコードの数が膨大となるので, scriptコマンドを用いて実行するバイトコードの番号を吐き出し, ログファイルを用いて比較する.

## MoarVMのデバッグ時のbreak point

- CbC側では次のオペコードの遷移は `cbc_next` というCodeGearで行う
- CodeGearは関数として扱える為, これに直接break pointを設定する

```
(gdb) b cbc_next
Breakpoint 2 at 0x7ffff7560288: file src/core
     /cbc-interp.cbc, line 61.
(gdb) command 2
Type commands for breakpoint(s) 2, one per
     line.
End with a line saying just "end".
>p CODES[*(MVMuint16 *)i->cur_op]
>p *(MVMuint16 *)i->cur_op
>c
>end
```
- オリジナルの場合マクロである為, dummy関数をマクロに記述し, この関数にbreakpointを設定する

```
dalmore gdb --args ../../MoarVM_Original/
     MoarVM/moar --libpath=src/vm/moar/stage0
     gen/moar/stage1/nqp
(gdb) b dummy
Function "dummy" not defined.
Make breakpoint pending on future shared
     library load? (y or [n]) y
Breakpoint 1 (dummy) pending.
(gdb) command 1
Type commands for breakpoint(s) 1, one per
     line.
End with a line saying just "end".
>up
>p *(MVMuint16 *)(cur_op)
>c
>end
```

## MoarVMのトレース

- トレース時には次の様なデバッグ情報の表示を利用する
- デバッガに, breakpointで停止した際のcur_opの値を表示する様に設定する.

```
Breakpoint 1, dummy () at src/core/interp.c
     :46
46 }
#1 0x00007ffff75608fe in MVM_interp_run (tc=0
     x604a20,
    initial_invoke=0x7ffff76c7168 <
        toplevel_initial_invoke>, invoke_data
        =0x67ff10)
    at src/core/interp.c:119
119 goto NEXT;
$1 = 159
Breakpoint 1, dummy () at src/core/interp.c
     :46
46 }
#1 0x00007ffff75689da in MVM_interp_run (tc=0
     x604a20,
    initial_invoke=0x7ffff76c7168 <
        toplevel_initial_invoke>, invoke_data
        =0x67ff10)
    at src/core/interp.c:1169
1169 goto NEXT;
$2 = 162
```

## アレ

```
100 MVM_STATIC_INLINE MVMint64 MVM_BC_get_I64(const MVMuint8 *cur_op, int offset) {
101     const MVMuint8 *const where = cur_op + offset;
102 #ifdef MVM_CAN_UNALIGNED_INT64
103     return *(MVMint64 *)where;
104 #else
105     MVMint64 temp;
106     memmove(&temp, where, sizeof(MVMint64));
107     return temp;
108 #endif
109 }
```

## MoarVMのデバッグ

- cur_opのみをPerlスクリプトなどを用いて抜き出し, 並列にログを取得したオリジナルと差分を図る
- この際に差異が発生したオペコードを確認し, その前の状態で確認していく

```
131 : 131
139 : 139
140 : 140
144 : 144
558 : 558
391 : 391
749 : 749
53 : 53
*54 : 8
```
## 現在のCbCMoarVM

- 現在はNQP, Rakudoのセルフビルドが達成でき, オリジナルと同等のテスト達成率を持っている
- moarの起動時のオプションとして `--cbc` を与えることによりCbCで動き, そうでない場合は通常のCで記述された箇所で実行される

## CbCMoarVMの利点

- バイトコードインタプリタの箇所をモジュール化する事が可能となり, CodeGearの再利用性や記述生が高まる
- デバッグ時にラベルではなくCodeGearにbreakpointを設定可能となり,デバッグが安易となる
- ThreadedCodeを実装する場合, CodeGearを組み合わせることにより実装する事が可能となる

## CbCMoarVMの欠点

- CbCコンパイラがバグを発生させやすく, 意図しない挙動を示す事がある
    - CbCコンパイラ自体のバグも存在する
- MoarVMのオリジナルの更新頻度が高い為, 追従していく必要がある
- CodeGear側からCに戻る際に手順が複雑となる
- CodeGearを単位として用いる事で複雑なプログラミングが要求される.

## ThreadedCodeの実装

- MoarVM内のオペコードに対応する処理が分離出来たことにより, オペコードに該当するCodeGearを書き連ねることによってThreadedCodeが実装可能となる


## CbCMoarVMと通常のMoarVMの比較

- CbCMoarVMと通常のMoarVMの速度比較を行った
- 対象として, 単純なループで数値をインクリメントする例題と, フィボナッチ数列を求める例題を選択した
- NQPで実装した場合とPerl6で実装した場合の速度を計測した

```
#! nqp

my $count := 100_000_000;

my $i := 0;

while ++$i <= $count {
}
```

```
#! nqp

sub fib($n) {
    $n < 2 ?? $n !! fib($n-1) + fib($n - 2);
}

my $N := 29;

my $t0 := nqp::time_n();
my $z  := fib($N);
my $t1 := nqp::time_n();

say("fib($N) = " ~ fib($N));
say("time    = " ~ ($t1-$t0));

```
# フィボナッチの例題

- フィボナッチの例題ではCbCMoarVMが劣る結果となった


## 単純ループ

- オリジナル
    - 7.499 sec
    - 7.844 sec
    - 6.074 sec
- CbCMoarVM
    - 6.135 sec
    - 6.362 sec
    - 6.074 sec

- 単純ループではCbCMoarVMの方が高速に動作する場合もある

## まとめ

- 速度を計測した所, 現在はCbCMoarVMの方が僅かに劣る結果となった
- ただしフィボナッチを求める例題などで, ケースによってはCbCMoarVMの方が高速に動作する場合もある


## まとめと今後の課題
- 継続と基本としたC言語 Continuation Based Cを用いてPerl6の処理系の一部を書き直した
- CbCの持つCodeGearによって, 本来はモジュール化出来ない箇所をモジュール化する事が出来た
- MoarVMの速度改善にはThreadedCodeが期待でき, CodeGearベースの命令ディスパッチとThreadedCodeは相性が良いと考えられる
- 今後は実行するバイトコードによりThreadedCode箇所と通常の配列を読み取り, 次のCodeGearを計算する処理を両立させていく