Closed6

JavaScriptのJITをざっくり理解する会

JJJJ

普段自分たちが書いているJavaScriptという言語はスクリプト言語であり、インタプリタで実行する。がそれは言語の制約ではなく実行環境の制約になるはず。

https://developer.mozilla.org/ja/docs/Web/JavaScript/About_JavaScript

現在JavaScriptの実行環境としてはNode.jsやChromiumに使われているV8というGoogle製のJavaScriptエンジンが特に知られているものになると思う

https://v8.dev/

このV8でどのようにJavaScriptを実行しているのかをざっくり調べた結果をここに書いていく。

JJJJ

まずV8は普段自分たちが書いたコードをいくつかのスレッド上で実行するっぽい。
1つはソースコードを取ってきてそれを実行するやつ。それ以外に最適化を行うためのスレッド、プロファイラ、GCなどがあるようだ。

この最適化というのが今回調べたいJITってやつ。
そもそもインタプリタとの差を把握したい。
今の自分の把握の中では、インタプリタは逐次実行をするもの、JITは実行よりも前の段階でコンパイルをしてコンパイル済みのコードを実行すると言った差があるように思える。

このため一般的にはJITが効いたものの方がパフォーマンスが良いとされており、V8もインタプリタで実行するのではなくJITをベースに実行して行っているっぽい

JJJJ

V8はコンパイラを2つ持っており、full-codegenってやつとcrankshaftって言うやつ?

full-codegenは性能の面では落ちるが素早くコンパイルできる君
crankshaftはより複雑な最適化をやってくれる君

まずはfull-codegenで実行が始まる。その間にプロファイラが最適化できそうなコードを探して、見つけたらcrankshaftが最適化を行なって次に同じメソッドを実行する時はcrankshaftが出力した最適化したコードを実行する形になる

JJJJ

crankshaftはコンパイラの内部表現として2つの種類のものを持っており、high-levelなものとlow-levelなものがあるらしい。
それぞれをhydrogenとlithiumと言うもの。
hydrogenは特定の機種に依存しない形のものの最適化を担って、lithiumはmips, arm, x86/x64向けの最適化をするためのものらしい

ref: http://nothingcosmos.github.io/V8Crankshaft/src/blog.html

v8の少し前のソースコード内にもcrankshaftの下にhydrogenとlithiumがある
https://chromium.googlesource.com/v8/v8/+/9fa206e1f4a36280672a4fb144cd7f78484b3c11/src/crankshaft

JJJJ

実際どのような最適化を行なっているのかを深掘りたい。まずはHidden Classと言われるやつ

JavaScriptの中でobjectを作る。例えば以下のようなコード

const obj = {}

これが作られた時点で C0 と名前をつけたHidden Classが生まれる。(C0という名前を慣習的につけてるらしい)

ここに新しくプロパティaを追加すると

obj.a = 1;

C1というのが作られて、 a のオフセットをこのC1というHidden Classは持っている。またC0は「C0にaと言うプロパティを追加した時にC1にtransitionする」という情報がはいる

次に

obj.b = 2;

のようなコードを書いた時にC2と言うHidden Classがうまれて、C1に「C1にbを追加した時にC2にtransition」すると言う情報がはいる。

このようにプロパティが追加された順番でHidden ClassはListのように情報を繋いでもっている(transition path?)

これをすることで同じ順番や、同じプロパティを参照した時などに無駄にHidden Classを作らないようにしている。以下の記事がすごく丁寧だった

https://engineering.linecorp.com/ja/blog/detail/232/

このHidden ClassはInline Cachingで使われるっぽい

Inline Cachingはあるメソッドが繰り返し呼ばれてる場合、同じ型のオブジェクトによく起こるという前提のもと行う最適化で最も最近呼び出されたメソッドの型をキャッシュしてその情報により将来そのメソッドが呼ばれた時の型の推論に活かすという技らしい
推論がうまくいけば、どのようにそのプロパティにアクセスするかを検索する時間を省くことが出来て、その型のhidden classの情報からオフセットを引っ張ってくることができると言うやつ

例えば

foo({ a: 1 })

と言うコードを実行した時点でプロパティaを持ったオブジェクトと言うhidden classが作られて

foo({ a: 2 })

みたいなコードを実行した時は、前にキャッシュして置いたhidden classと一致することからプロパティaへのアクセスの方法を検索する必要がなくなるため高速化されると言う感じ

JJJJ

つまりJITに優しいコードを書く(自分たちが気をつけることができる)書き方ってどうしたら良いんやって言うと

  • オブジェクトのプロパティの順番を揃える(これにより無駄にhidden classを作らなくて済むし、inline cachingを効かせれる)
  • 動的にプロパティを追加しない(動的に追加してしまうとhidden classを作ってしまうので、初期化のタイミングで使うプロパティを先に列挙して置くことによりhidden classを作らなくて済むようにする)
  • 多数のメソッドを使って1回で実行するより、同じメソッドを複数回つかって実行できる形の方がinline cachingを効かせられるので速い

参考にした記事
https://postd.cc/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code/

このスクラップは2021/02/12にクローズされました