Dart VM Overview 1.0 documentation

Dart VM Advent Calendar 2012 12/07

«  Dart VM Advent Calendar 2012 12/05   ::   Contents   ::   Dart VM Advent Calendar 2012 12/08  »

Dart VM Advent Calendar 2012 12/07

VM Advent Calendar 2012/12/07

Dart VMの概要(3)

前回の続き、fiboが2000回以上よばれて最適化JITコンパイルされるところから!!!

Dart VMは、2000回以上実行された関数は、returnする際にOptimizeFunctionを呼び出し、最適化JITコンパイルします。

stub_OptimizeFunction自体は、Dart VMのRuntime API(OptimizeInvokedFunction)を呼び出すためのstubです。

Returnのコード

0xb30482b1    ba11042db3             mov edx,0xb32d0411  'Function 'fibo': static.'
0xb30482b6    ff422b                 inc [edx+0x2b]       // fiboのusage_counterをinc
0xb30482b9    817a2bd0070000         cmp [edx+0x2b],0x7d0 // 2000と比較してhotcode check
0xb30482c0    7c05                   jl 0xb30482c7
0xb30482c2    e8e185ffff             call 0xb30408a8  [stub: OptimizeFunction]

stub: OptimizeFuntion:

Code for stub '_stub_OptimizeFunction': {
0xb30408a8    55                     push ebp
0xb30408a9    89e5                   mov ebp,esp
0xb30408ab    6800000000             push 0
0xb30408b0    50                     push eax
0xb30408b1    52                     push edx
0xb30408b2    b950070b08             mov ecx,0x80b0750
0xb30408b7    ba01000000             mov edx,0x1
0xb30408bc    e867f73702             call 0xb53c0028  [stub: CallToRuntime]
0xb30408c1    5a                     pop edx
0xb30408c2    58                     pop eax
0xb30408c3    89ec                   mov esp,ebp
0xb30408c5    5d                     pop ebp
0xb30408c6    c3                     ret

Dart VMは、JITコンパイルしたコードに、Dart VMが提供するRuntime APIを呼び出すStubを埋め込みます。

fiboの中間表現(非最適化時)

==== file:///home/elise/language/dart/work/adven/fibo.dart_::_fibo
B0[graph]
B1[target]
    CheckStackOverflow:2()
    t0 <- LoadLocal:3(n lvl:0)                        //変数nをLoad(引数という概念はここではない)
    t1 <- Constant:4(#2)                              //定数2を生成
    Branch if RelationalOp:5(<, t0, t1) goto (2, 3)   //(t0 < t1)だったら、B2へjump、もしくはB3へjump
B2[target]
    t0 <- LoadLocal:7(n lvl:0)                        //定数nをLoad
    Return:8(t0)                                      //Return
B3[target]
    t0 <- LoadLocal:9(n lvl:0)                        //変数nをLoad
    PushArgument:10(t0)                               //nを引数にpush
    t0 <- Constant:11(#1)                             //定数1を生成
    PushArgument:12(t0)                               //#1を引数にpush
    t0 <- InstanceCall:13(-, t0, t0)                  //InstanceCall (-)(n, #1)みたいなメソッドコール
    PushArgument:14(t0)                               //返値をpush        [`n-1`]
    t0 <- StaticCall:15(fibo t0)                      //fibo(n-1)に相当
    PushArgument:16(t0)                               //返値をpush        [`fibo(n-1)`]
    t0 <- LoadLocal:17(n lvl:0)                       //変数nをLoad
    PushArgument:18(t0)                               //nをpush
    t0 <- Constant:19(#2)                             //定数2を生成
    PushArgument:20(t0)                               //#2をpush
    t0 <- InstanceCall:21(-, t0, t0)                  //InstanceCall (-)(n, 2)みたいなメソッドコール
    PushArgument:22(t0)                               //返値をpush        [`fibo(n-1)`, `n-2`]
    t0 <- StaticCall:23(fibo t0)                      //fibo(n-2)に相当
    PushArgument:24(t0)                               //返値をpush        [`fibo(n-1)`, `fibo(n-2)`]
    t0 <- InstanceCall:25(+, t0, t0)                  //InstanceCall (+)(`fibo(n-1)`, `fibo(n-2)`)
    Return:26(t0)                                     //返値をreturn

JITコンパイル(非最適化)とJITコンパイル(最適化)の中間表現を比較してみます。

fiboの中間表現(最適化時、型情報のフィードバック後)

==== file:///home/elise/language/dart/work/adven/fibo.dart_::_fibo
B0[graph] {
      v0 <- Constant:29(#null)
      v1 <- Parameter:30(0)
}
B1[target]
    CheckStackOverflow:2()
    v2 <- Constant:4(#2)
    Branch if RelationalOp:5(<, v1, v2 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #588]) goto (2, 3) env={ v1, v1, v2 }
B2[target]                                   ^ (<)のreceiverがSmi  ^ arg1がSmi
    Return:8(v1)
B3[target]
    PushArgument:10(v1)
    v3 <- Constant:11(#1)
    PushArgument:12(v3)
    v4 <- InstanceCall:13(-, v1, v3 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #309]) env={ v1, a0, a1 }
    PushArgument:14(v4)                   ^ (-)のreceiverがSmi  ^ arg1がSmi
    v5 <- StaticCall:15(fibo v4) env={ v1, a0 }
    PushArgument:16(v5)
    PushArgument:18(v1)
    v6 <- Constant:19(#2)
    PushArgument:20(v6)
    v7 <- InstanceCall:21(-, v1, v6 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #278]) env={ v1, a0, a1, a2 }
    PushArgument:22(v7)                   ^ (-)のreceiverがSmi ^ arg1がSmi
    v8 <- StaticCall:23(fibo v7) env={ v1, a0, a1 }
    PushArgument:24(v8)
    v9 <- InstanceCall:25(+, v5, v8 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #274]) env={ v1, a0, a1 }
    Return:26(v9)                         ^ (+)のreceiverがSmi ^ arg1がSmi

非最適化のfiboのコードを実行した際に、InlineCacheは値がSmi型であると記録しておきます。

最適化JITコンパイル時には、その型情報をフィードバックして最適化を行います。

型情報は、receiverとargsのtripleで記録されます。

仮に、複数の型が混在する場合、

[PT: _Smi, _Smi | _Smi, _Double | _Double, _Smi]のような表現になるはずです。

Note

一言断っておくと、JITコンパイル(非最適化)の中間表現は非SSA形式です。

JITコンパイル(最適化)の中間表現はSSA形式になるため、 IRのdefinevalueはtからvに変わって、重ならないようにnumberingされます。

fiboの中間表現(最適化後)

==== file:///home/elise/language/dart/work/adven/fibo.dart_::_fibo
  0: B0[graph] {
      v0 <- Constant:29(#null)
      v1 <- Parameter:30(0) {PT: dynamic} {PCid: dynamic}
}
  2: B1[target] ParallelMove eax <- S-1
  4:     CheckStackOverflow:2()
  6:     v2 <- Constant:4(#2) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [2, 2]
  8:     CheckSmi:5(v1) env={ v1 [eax], v1 [eax], v2 [C] }
 10:     Branch if RelationalOp:5(<, v1, v2 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #588]) goto (2, 3)
 12: B2[target]
 13:     ParallelMove eax <- eax
 14:     Return:8(v1)
 16: B3[target]
 18:     v3 <- Constant:11(#1) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [1, 1]
 20:     ParallelMove ecx <- eax
         //v4 <- InstanceCall:13(-, v1, v3 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #309]) env={ v1, a0, a1 }
 20:     v4 <- BinarySmiOp:13(-, v1, v3) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [1, 1073741822] -o        <-- Smi型に特殊化された中間表現
 22:     PushArgument:14(v4) {PCid: dynamic}
         //v5 <- StaticCall:15(fibo v4) env={ v1, a0 }
 24:     v5 <- StaticCall:15(fibo v4) {PT: dynamic} {PCid: dynamic} env={ v1 [S-1], a0 }
 25:     ParallelMove eax <- eax
 26:     ParallelMove ecx <- S-1, S+0 <- eax
         //v7 <- InstanceCall:21(-, v1, v6 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #278]) env={ v1, a0, a1, a2 }
 26:     v7 <- BinarySmiOp:21(-, v1, v2) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [0, 1073741821] -o        <-- Smi型に特殊化された中間表現
 28:     PushArgument:22(v7) {PCid: dynamic}
         //v8 <- StaticCall:23(fibo v7) env={ v1, a0, a1 }
 30:     v8 <- StaticCall:23(fibo v7) {PT: dynamic} {PCid: dynamic} env={ v1 [S-1], v5 [S+0], a0 }
 31:     ParallelMove ecx <- eax, eax <- S+0
 32:     CheckSmi:25(v5) env={ v1 [S-1], v5 [eax], v8 [ecx] }
 34:     CheckSmi:25(v8) env={ v1 [S-1], v5 [eax], v8 [ecx] }
 36:     ParallelMove edx <- eax
         //v9 <- InstanceCall:25(+, v5, v8 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #274]) env={ v1, a0, a1 }
 36:     v9 <- BinarySmiOp:25(+, v5, v8) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [-inf, +inf] +o env={ v1 [S-1], v5 [eax], v8 [ecx] } <-- Smi型に特殊化された中間表現
 37:     ParallelMove eax <- edx
 38:     Return:26(v9)

最適化後の中間表現です。最適化により様々な変化があります。

  1. 型情報のフィードバック結果から型推論を行う。
  2. CheckSmi命令の挿入
  3. 型推論の結果、高速な中間表現に置き換える。
  4. スタックとレジスタ割付けが行われ、ParallelMove命令に置き換える。

高速な命令に置き換える、では、Smi型でのみ呼び出すInstanceCallをBinarySmiOpに置き換えています。

BinarySmiOpは、receiverとarg1がSmi型である場合に置き換えられる(特殊化)命令になります。

BinarySmiOpは、最終的にアセンブラ数命令に変換されます。

Type Guard

Check命令の挿入は、少々特殊なことを行います。

24:     v5 <- StaticCall:15(fibo v4)
30:     v8 <- StaticCall:23(fibo v7)
32:     CheckSmi:25(v5) env={ v1 [S-1], v5 [eax], v8 [ecx] }  <-- CheckSmi
34:     CheckSmi:25(v8) env={ v1 [S-1], v5 [eax], v8 [ecx] }  <-- CheckSmi
36:     ParallelMove edx <- eax
36:     v9 <- BinarySmiOp:25(+, v5, v8) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72}

上記は、v5とv8をCheckSmi命令で、Smi型であることを保証した上で(Smi型のGuard) BinarySmiOp(+, v5, v8)を行っています。

v5とv8は、StaiticCall(fibo)の返値であり、Smi型である保証がないため、CheckSmiで確認を行っています。

BinarySmiOpはSmi型でしか動作しない命令であるため、 Smi型以外が入ってきたら異常終了してしまう可能性があります。

仮に、StaticCall(fibo)の返値がdouble型やnull型だった場合、CheckSmiにひっかかってしまいます。

Smi型以外だった場合、CheckSmiは脱最適化(Deoptimization)を行い、この最適化したコードを捨て、 元のどんな型でも動く非最適化コードに戻します。

型情報のフィードバックを受けたこの最適化JITコンパイルが生成したコードは、 Smi型が入ってくる限り非常に高速に動作します。

fiboのアセンブラ(最適化)

Code for optimized function 'file:///home/elise/language/dart/work/adven/fibo.dart_::_fibo' {
0xb3048308    55                     push ebp              //  v0 <- Constant:29(#null)
0xb3048309    89e5                   mov ebp,esp           //  v1 <- Parameter:30(0) {PT: dynamic} {PCid: dynamic}
0xb304830b    e800000000             call 0xb3048310
0xb3048310    83ec04                 sub esp,0x4
0xb3048313    8b4508                 mov eax,[ebp+0x8]     //  2: B1[target] ParallelMove eax <- S-1
0xb3048316    3b257c7a6b08           cmp esp,[0x86b7a7c]   //  4:     CheckStackOverflow:2()
0xb304831c    0f8668000000           jna 0xb304838a
                                                           //  8:     CheckSmi:5(v1) env={ v1 [eax], v1 [eax], v2 [C] }
0xb3048322    a801                   test al,0x1           // check SmiTag
0xb3048324    0f8573000000           jnz 0xb304839d        // goto DeoptimizeStub
                                                           //  6:     v2 <- Constant:4(#2) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [2, 2]
                                                           // 10:     Branch if RelationalOp:5(<, v1, v2 IC[1: _Smi@0x36924d72, _Smi@0x36924d72 #588]) goto (2, 3)
0xb304832a    83f804                 cmp eax,0x4           // if n < 2  // (n<<1 < 2<<1)
0xb304832d    0f8d05000000           jnl 0xb3048338        // goto B3[target]
0xb3048333    89ec                   mov esp,ebp           // 14:     Return:8(v1)
0xb3048335    5d                     pop ebp
0xb3048336    c3                     ret
0xb3048337    90                     nop                   // 16: B3[target]
                                                           // 18:     v3 <- Constant:11(#1) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [1, 1]
                                                           // 20:     ParallelMove ecx <- eax
                                                           // 20:     v4 <- BinarySmiOp:13(-, v1, v3) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [1, 1073741822] -o
0xb3048338    89c1                   mov ecx,eax
0xb304833a    83e902                 sub ecx,0x2           // n - 1  (n<<1 - 1<<1)
0xb304833d    51                     push ecx              // 22:     PushArgument:14(v4) {PCid: dynamic}
0xb304833e    bab16b0fb3             mov edx,0xb30f6bb1    // 24:     v5 <- StaticCall:15(fibo v4) {PT: dynamic} {PCid: dynamic} env={ v1 [S-1], a0 }
0xb3048343    e8c0803702             call 0xb53c0408  [stub: CallStaticFunction]  // call fibo(n-1)
0xb3048348    83c404                 add esp,0x4
0xb304834b    8b4d08                 mov ecx,[ebp+0x8]     // 26:     ParallelMove ecx <- S-1, S+0 <- eax
0xb304834e    8945f8                 mov [ebp-0x8],eax
                                                           // 26:     v7 <- BinarySmiOp:21(-, v1, v2) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [0, 1073741821] -o
0xb3048351    83e904                 sub ecx,0x4           // n - 2 (n<<1 - 2<<1)
0xb3048354    51                     push ecx              // 28:     PushArgument:22(v7) {PCid: dynamic}
0xb3048355    bab16b0fb3             mov edx,0xb30f6bb1    // 30:     v8 <- StaticCall:23(fibo v7) {PT: dynamic} {PCid: dynamic} env={ v1 [S-1], v5 [S+0], a0 }
0xb304835a    e8a9803702             call 0xb53c0408  [stub: CallStaticFunction]  // call fibo(n-2)
0xb304835f    83c404                 add esp,0x4
0xb3048362    89c1                   mov ecx,eax           // 31:     ParallelMove ecx <- eax, eax <- S+0
0xb3048364    8b45f8                 mov eax,[ebp-0x8]
                                                           // 32:     CheckSmi:25(v5) env={ v1 [S-1], v5 [eax], v8 [ecx] }
0xb3048367    a801                   test al,0x1           // check SmiTag
0xb3048369    0f8533000000           jnz 0xb30483a2        // goto DeoptimizeStub
                                                           // 34:     CheckSmi:25(v8) env={ v1 [S-1], v5 [eax], v8 [ecx] }
0xb304836f    f6c101                 test_b ecx,0x1        // check SmiTag
0xb3048372    0f852f000000           jnz 0xb30483a7        // goto DeoptimizeStub
0xb3048378    89c2                   mov edx,eax           // 36:     ParallelMove edx <- eax
                                                           // 36:     v9 <- BinarySmiOp:25(+, v5, v8) {PT: _Smi@0x36924d72} {PCid: _Smi@0x36924d72} [-inf, +inf] +o env={ v1 [S-1], v5 [eax], v8 [ecx] }
0xb304837a    03d1                   add edx,ecx           // fibo(n-1) + fibo(n-2)
0xb304837c    0f802a000000           jo 0xb30483ac
0xb3048382    89d0                   mov eax,edx           // 37:     ParallelMove eax <- edx
0xb3048384    89ec                   mov esp,ebp           // 38:     Return:26(v9)
0xb3048386    5d                     pop ebp
0xb3048387    c3                     ret
//Runtime
0xb3048388    90                     nop
0xb3048389    cc                     int3
0xb304838a    50                     push eax
0xb304838b    b9f0c20a08             mov ecx,0x80ac2f0
0xb3048390    ba00000000             mov edx,0
0xb3048395    e88e7c3702             call 0xb53c0028  [stub: CallToRuntime]
0xb304839a    58                     pop eax
0xb304839b    eb85                   jmp 0xb3048322
0xb304839d    e8c6813702             call 0xb53c0568  [stub: Deoptimize]
0xb30483a2    e8c1813702             call 0xb53c0568  [stub: Deoptimize]
0xb30483a7    e8bc813702             call 0xb53c0568  [stub: Deoptimize]
0xb30483ac    e8b7813702             call 0xb53c0568  [stub: Deoptimize]
0xb30483b1    e972813702             jmp 0xb53c0528  [stub: FixCallersTarget]
0xb30483b6    e94d823702             jmp 0xb53c0608  [stub: DeoptimizeLazy]
}



非最適化のコードと比較して、高速なコードを生成しています。

特に、InlineCacheのStub呼び出しをしていたところは、すべて高速な命令に置き換えられており、 BinarySmiOpは、アセンブラ1-2命令になっています。

またBinarySmiOpでは、両者ともSmi型であるため、1bit左シフト済みの値が入っているはずですが、

右シフトで元の値に戻さずに、add sub cmp命令を行っています。

print(ret)など、int値を参照する場合には、右シフトが挿入されるはずです。

Dart VMの初期化、JITコンパイル(非最適化)、JITコンパイル(最適化)、fibo(40)の計算まで、 大体900msecくらいなので、かなり速いと思います。

corei7(ia32)にて、

dart$ time dart --time-all fibo.dart
102334155
ret = 897 ms                                 <-- fibo(40)の処理時間
Script Loading :  93 micros.                 <-- Dart VMがソースコードを読み込んだ合計時間
Snapshot Creation :  0 micros.               <-- Snapshotの合計時間、最後に行うんだっけかな?
Isolate initialization :  11019 micros.      <-- Isolateの初期化時間
Function compilation :  11742 micros.        <-- Compileの合計時間 main, fibo以外のcoreも含む。
Bootstrap of core classes :  28 micros.      <-- Bootstrap時の、core libのdeserialize処理時間
Total runtime for isolate :  909578 micros.  <-- main以降の合計時間

real  0m0.924s                               <-- VMの起動時間は、25,000 microsくらいかな?
user  0m0.920s
sys   0m0.000s

まとめ

  1. 最適化JITコンパイルは、型情報のフィードバックをもらう。
  2. 最適化JITコンパイルは、特殊化された中間表現に置き換える。
  3. 特殊化された中間表現は、少ないアセンブラ命令に落ちる。

«  Dart VM Advent Calendar 2012 12/05   ::   Contents   ::   Dart VM Advent Calendar 2012 12/08  »