abcdefGets

ゲッツ!

2019 Javascript engine 俯瞰

2019 Javascriptエンジン俯瞰

こんにちは 2019 Javascript Advent Calendarの11日目です

2019はJSエンジンが新たに2つもリリースされた
まずFacebook産のhermes
もう一つがFFMPEG作者のbellardが実装したquickjs

この2つを見ていこうと思う

ちなみにhermesは以前にも書いたので正直あまり書くことは無い
http://abcdef.gets.b6n.ch/entry/2019/07/22/142510

特徴

hermes

  • C++
  • FacebookがReact Nativeの高速化用に実装したエンジン
  • レジスタマシンのバイトコードインタプリタを搭載
  • flowを解釈できる
  • commonjsを解釈して実行できる
  • バイトコードのexportとimportも可能でスタートアップタイムを高速化することが可能
  • JITx86_64の実装はあるが使うパスが無いためOFFになっている
  • let/constやclass、ES Moduleといった機能はサポートされていない
  • Reflectionやwith、Symbol.speciesといったものは今後もサポートしない
  • 一部Ecma262と仕様が異なる

quickjs

実装

パーサ

両方ともシンプルな手書きの再帰下降構文解析でパース

hermes

hermesはパースしながらASTを生成する
hermesのASTはESTreeの仕様に準拠しているのでわかりやすい

quickjs

quickjsは非常にドラスティックな実装となっており、パースしながらなんとBytecodeを直接出力する
ファイルサイズ削減のためにASTをスキップしていて、完全にコンパクト方面に振り切っている感じ
ここらへんはエンジンの色がでていて面白い

Bytecode

hermes

hermesバイトコードBytecodeList.defに全てまとまっており、全てマクロ呼び出しで後々再利用できるようになっている
hermesバイトコードは可変長で、オペランド数はMaxが6、オペランドのサイズも可変長
NewObject等の割と大きめな命令から、BitXorの様なプリミティブ命令まで実装されている

quickjs

対するquickjsバイトコードquickjs-opcode.hにすべてまとまっており、こちらも同じくマクロ呼び出しとなっている
まあ言語エンジンを実装するときは大体こうなるよね
quickjsバイトコードだが、まあ大体hermesと同じような粒度かな
一部push_i32のようなスタックマシンぽいコードもあったり

Bytecodeは両者ともわりかし似ている感じ

Object Model

hermes

以前も書いたが、hermesはNaN-Boxingで実装されている(NaN-BoxingについてはFacebookのHermes Javascript Engineについてにも書いた)
hermesのヒープオブジェクトはGCCellクラスを頂点としたモデルになっている

GCCell
|
+--------+------+-----------------------+-----------+----------------+------------+--------------+--------------+-----+-----+---------+
|        |      |                       |           |                |            |              |              |     |     |         |
JSObject Domain VariableSizeRuntimeCell HiddenClass PropertyAccessor HashMapEntry OrderedHashMap OrderedHashMap Type1 Type2 EmptyCell FinalizerCell
|
|--------+--------------+------------------------------------+---------+---------------+-------------+----------+------+-------+-----------+---------+-----------------+--------+----------------+-----------------+------------+----------------+
|        |              |                                    |         |               |             |          |      |       |           |         |                 |        |                |                 |            |                |
Callable RequireContext HostObject                           ArrayImpl JSArrayIterator JSArrayBuffer JSDataView JSDate JSError JSGenerator JSMapImpl JSMapIteratorImpl JSRegExp JSTypedArrayBase JSWeakMapImplBase PrimitiveBox JSStringIterator SingleObject
|                                                            |                                                                                                                  |                |                 |
+-------------+-----------------------------------+          +---------+                                                                                                        |                |                 +--------+--------+---------+
|             |                                   |          |         |                                                                                                        |                |                 |        |        |         |
BoundFunction NativeFunction                      JSFunction Arguments JSArray                                                                                                  JSTypedArray     JSWeakMapImpl     JSString JSNumber JSBoolean JSSymbol
              |                                   |
              NativeConstructor NativeConstructor JSGeneratorFunction GeneratorInnerFunction

JSObject階層以外は省略したが、hermesは上記の様なオブジェクト階層を持っておりまあ割と複雑
GCCellクラスはVTableというオブジェクトに関する情報を保持しているクラスをラップしており、VTableが実際にGC用の型情報やマーク情報を持っている
ただ、VTableクラス自体はGCによってアドレスが変わる可能性があるため、GCCellクラスがハンドルの役目を果たしている
そのため、GCCellはメンバを持たない

ランタイムでのオブジェクトの型情報はその名の通りHiddenClassが保持しており、V8っぽくHiddenClassの中にプロパティのキャッシュを持っていたりする
当然プロパティ追加時のTransitionも実装されていて、ちゃんとhidden classとして機能している

{symbol_id} = internal variable name id
{property_flag} = enumerable|writable|configurable|...

+------------------------+
| HiddenClass(root)      |
+------------------------+
            |
            | (Transition HiddenClass(propertyA))
            |
+------------------------+
| HiddenClass(propertyA) |
+------------------------+
             | (TransitionMap {symbol_id}_{property_flag}: HiddenClass(propertyB), {symbol_id}_{property_flag}: HiddenClass(propertyC)})
             +-------------------------+
             |                         |
+------------------------+ +------------------------+
| HiddenClass(propertyB) | | HiddenClass(propertyC) |
+------------------------+ +------------------------+

こんな風に1つの派生だけの場合はTransitionオブジェクトを直接参照して、複数のTransitionがある場合はsymbol_idproperty_flagのキーとHiddenClassを直接ハッシュマップとして持つことでTransitionを実現している

quickjs

quickjsはCなので明示的なオブジェクト階層はないもののJSObjectが多くのJS型のベースとなっておりメンバをunionを選択する形となる
またJSValueという空のstructGenericな値として利用しており、内部に持つ値はvoid*ではなくJSValue*型で保持している

JSObject
|
+-JSBoundFunction
+-JSCFunctionDataRecord
+-JSForInIterator
+-JSArrayBuffer
+-JSTypedArray
+-JSFloatEnv
+-JSMapState
+-JSMapIteratorData
+-JSArrayIteratorData
+-JSRegExpStringIteratorData
+-JSGeneratorData
+-JSProxyData
+-JSPromiseData
+-JSPromiseFunctionData
+-JSAsyncFunctionData
+-JSAsyncFromSyncIteratorData
+-JSAsyncGeneratorData
+-func
+-cfunc
+-JSTypedArray
+-array
+-JSRegExp

こんな感じでJSObjectにunionを持っている
クラシックなunionベースのモデルなので読みやすくていい

JSObjectはルートの構造体となるので、型情報や、GCMark等の情報を保持している

型情報は全てJSObject内のJSShapeというstructに持っている

JSShapeはプロパティ自体も保持しているが、現在のプロパティのバージョンをハッシュとしても持っており、プロパティが変化することでJSShape内のハッシュ値も変化する
さらに次のハッシュ値をプロパティのメモリアドレスから計算して、前のJSShapeへ持たせることで、次のJSShapeを検索することが可能になり、Transitionを実現している

+-----------+
| JSShape A |             |A(next_hash = null)| ...
+-----------+
     |
     | Add property
     |
+-----------+
| JSShape B |             |A(next_hash = 3)   | |B(next_hash = null)|
+-----------+

だいぶ簡略化して描いているが、next_hashに次のインデックスを格納するイメージ(本当は直接インデックスは格納しない)

VM

hermes

再掲となるが
VMで利用されるレジスタは一応無限の仮想レジスタとなっているが、単純なリニア生存区間解析をCFG上で行っている
一応無制限と書いたのは、Bytecodeのレジスタインデックスが1byteしか受けつけないので、実質256までしかレジスタ割付ができないからである

https://github.com/facebook/hermes/blob/master/doc/Design.mdに書いてあるが、Facebook調べでは256以上のレジスタを使う関数は 見当たらなかったらしい(ので大丈夫ということか)

quickjs

スタックマシンなので単純にstack pointerを伸縮している

両者ともラベルアドレスへのダイレクトジャンプを実装している(もちろんラベルのアドレスが取得できないコンパイラ向けのswitch-caseもある)
ちなみにどんな機能かというと

Label_A: {
  ...
}

Label_B:
.
.
.

const void* dispatch_tables = {&&Label_A, ...}

goto *dispatch_tables[1]

とすることでなんとlabelへとgotoできるという素敵な仕様
これを使うことでジャンプの度にcmpする必要がなくなる(indexの計算のみ)さらにCPUの分岐予測もいらないので、VMのループが高速化される

GC

hermes

GenerationalMark-Sweep GCでしたそれ以外特に言うこともないかな
オブジェクトグラフをひたすらVisitorがたどっていくprecise GCの実装となっていた

quickjs

こっちはちょっと特殊で参照カウント + mark-sweepとなっている
最初にオブジェクトグラフを巡回して参照カウントをdecrefしたのち、参照カウントが0のオブジェクトをdelete_listに詰め込んで回り、全て回収完了後にJSObjectを解放する
Pythonの方式と同じ方法でオブジェクトを削除している

すべての割り当てられたオブジェクトはobj_list内にあるので、それをtmp_obj_listにコピーして、
巡回中の参照カウントが1以上のオブジェクトはobj_listに戻す

+----------------+
|   global_env   |
+----------------+
        |
        |         +----------------+
        |         |  tmp_obj_list  |
        |         +----------------+
        |                 |
        |                 |     +------------+        +------------+
        |                 |-----|  objectA 1 |------->|  objectB 1 |
        |                 |     +------------+    |   +------------+
        |                 |                       |
        |                 |                       |   +------------+
        |                 |                       +-->|  objectC 2 |
        |                 |                           +------------+
        |      REF        |     +------------+              ↑
        |==============>  |-----|  objectD 2 |--------------+
                          |     +------------+
                          |
                          |                           +------------+  REF
                          |                      ---->|  objectF 2 |<=====+
                          |     +------------+   |    +------------+      |
                          |-----|  objectE 1 |---+                        |
                                +------------+   |    +------------+  REF |
                                                 ---->|  objectG 2 |<=====+
                                                      +------------+

これが巡回前 参照されているのはすでにObjectDのみの状態で、objectFとobjectGは循環参照している

+----------------+
|   global_env   |
+----------------+
        |
        |         +----------------+
        |         |  tmp_obj_list  |
        |         +----------------+
        |                 |
        |                 |     +------------+        +------------+
        |                 |-----|  objectA 0 |------->|  objectB 0 |
        |                 |     +------------+    |   +------------+
        |                 |                       |
        |                 |                       |   +------------+
        |                 |                       +-->|  objectC 1 |
        |                 |                           +------------+
        |      REF        |     +------------+              ↑
        |==============>  |-----|  objectD 1 |--------------+
                          |     +------------+
                          |
                          |                           +------------+  REF
                          |                      ---->|  objectF 1 |<=====+
                          |     +------------+   |    +------------+      |
                          |-----|  objectE 0 |---+                        |
                                +------------+   |    +------------+  REF |
                                                 ---->|  objectG 1 |<=====+
                                                      +------------+

これが巡回後
親が外部から参照されているオブジェクト以外は参照カウントが0になる、そして親が0の場合は回収されるので、循環していても関係なく削除できるようになる

ちなみにquickjsはメモリ割り当てに通常のmallocを使用していて、mallocの管理ヘッダ分のサイズも計算して割当中のオブジェクトサイズを保持している
メモリを回収する場合も単純にfreeを呼び出しており、メモリ管理は基本的にすべてglibcにまかせている

まとめ

簡単に2つのエンジンを俯瞰したが、quickjsは軽い実装ながら面白い箇所が多く、またコードもきれいで読みやすいのでおすすめ
quickjs.cにほぼ全てのコードが書かれているので読むのも簡単だと思う
hermesは普通に読むの面倒だし、そんなに面白いことも無いかもしれない

疲れました

では