ジャスト・イン・タイム (JIT) コンパイラの短期集中コース
JavaScript はブラウザ上でどのように動作しているか
開発者として JavaScript を追加すると、ゴールと問題が出てきます。
ゴール : コンピュータへ何かを伝えたいと思います
問題 : あなたとコンピュータは別の言語を話します
あなたは人間の言語を話し、コンピュータは機械語を話します。JavaScript や他の高レベルのプログラミング言語を人間の言語として考えていなくても、実際には人間の言葉です。これらは人間の認知のために開発されたものであり、コンピュータの認知のために考えられたものではありません。
JavaScript のエンジンの仕事は人間の言葉を機械が理解できるように変換することです。
人間とエイリアンがお互いに話そうとしている映画、「メッセージ」のように考えています。
この映画では、人間とエイリアンは一語一語の翻訳をするわけではありません。2つのグループは世界に関して別の考え方を持っています。そして人間と機械に関してもそうです (次の記事でこの映画に関してはもっと説明します) 。
では翻訳はどのように行いますか?
プログラミングでは、機械語に翻訳するには一般的に 2 つの方法があります。
インタープリタでは、この翻訳は直ちに一行一行実施します。
一方コンパイラは直ちに翻訳を行いません。翻訳を作成して、それを書きとめるために、事前に動作します。
インタープリタの長所と短所
インタープリタはすぐに起動して実行できます。コードの実行を開始する前に、コンパイルの手順全体を踏む必要はありません。 最初の行を翻訳して実行するだけです。
そしてそれが様々なブラウザが最初から JavaScript インタープリタを使用していた理由です。
しかし同じコードを何度も実行するときに毎回翻訳が必要というインタープリタの問題があります。例えば、ループを使用するときがそうです。この時同じ翻訳を何度も何度も実行する必要があります。
コンパイラの長所と短所
コンパイラは対照的なトレードオフを持っています。
開始時にコンパイルを行うステップを経ないといけないため、起動に少し時間がかかります。 しかし、ループ内のコードパスはそのループを通過するたびに翻訳を繰り返す必要がないため、実行速度が速くなります。
もう一つの違いはコンパイラはコードの中身を確認し、より速く動作するように編集を行う時間があることです。これらの編集は最適化と呼ばれています。
ジャスト・イン・タイム コンパイラ : 2つの世界の最も優れたもの
別のブラウザではこの方法はやや異なっていますが、基本的なアイデアは同じです。モニター (別名プロファイラ) と呼ばれる 新しい機能を JavaScript エンジンへ追加しました。そのモニターは実行中のコードを監視し、何回動作したかとどのようなタイプで使われるのかを記録します。
まず最初にモニターはインタープリタを通してすべてを実行します。
もし同じ行を何度か通った場合、コードのその部分は “warm” と呼ばれます。もし何度も動作する場合、ここは “hot” と呼ばれます。
ベースラインコンパイラ
関数が “warm” な状態になると、JIT はそれを送信しコンパイルします。そしてコンパイル結果を保存します。
それはスピードアップするのに役立ちます。しかし、先程述べたように、コンパイラができることはもっとあります。最適化を行うための最も効率的なやり方を理解するまでには時間がかかることがあります。
ベースラインコンパイラはこれらの最適化の幾つかを行います (以下に例を示します )。長い時間作業を保留にしたくないため、これらに多くの時間をかけたくありません。
しかしコードが本当に “hot” な場合、—もしそれがいっぱい実行されていたら— このような時にさらなる最適化を実施する時間を余分にとる価値があります。
コンパイラの最適化
コードの一部がとても “hot” な時、モニターは最適化コンパイラに対してそれを送信します。これにより、保存される関数の別のより高速なものが作成されます。
コードのより高速なバージョンを作るために、最適化コンパイラはいくつかの過程を行う必要があります。
例えば、特定のコンストラクタによって作成されたすべてのオブジェクトが同じ形状を持っていると仮定することができます。つまり、それらは常に同じプロパティ名を持ち、それらのプロパティは同じ順序で追加され、それに基づいていくつかのコーナーをカットすることができます。
最適化コンパイラはモニターが監視したコードの実行状態の情報を使用します。ループが通る前のすべてのパスが真だった場合、それらは引き続き真だとみなされます。
もちろん JavaScript を使用した場合、それらの保証はありません。あなたが 99 個の同じ形状のオブジェクトを持ってしたとしても、100 個目のオブジェクトはプロパティが無いかもしれません。
従ってコンパイルされたコードは仮定が正しいかどうかを動作させる前に確認する必要があります。もし正しければコンパイルされたコードを動作させます。もし正しくなければ、JIT は誤った仮定とみなし、最適化したコードを破棄します。
たいていの最適化コンパイラはコードを速く動くようにしますが、時々期待されないパフォーマンスの問題が発生します。最適化した上で最適化解除することを続けるコードがある場合、ベースラインのコンパイル済みバージョンを実行するよりも遅くなります。
ほとんどのブラウザでは最適化/最適化解除のサイクルに問題が発生した時、終了させる制限を加えています。もし JIT が最適化を 10 回以上試行しそれを捨て続けた場合、試行をやめます。
最適化の例 : 型の特殊化
JavaScriptが利用する動的型システムでは、実行時に余分な作業が必要です。例えば、次のコードで考えてみましょう。
function arraySum(arr) { var sum = 0; for (var i = 0; i < arr.length; i++) { sum += arr; } }
arr
が 100 個の整数の配列であるとしましょう。コードが “warm up” すると、ベースラインコンパイラは関数内の各演算のスタブを作成します。そのため sum += arr
のスタブがあり、これは += 演算を整数加算として扱います。
sum
と arr
は整数であるとは限りません。 JavaScript では型が動的なので、ループの後の反復で、arr
が文字列になる可能性があります。 整数の加算と文字列の連結は非常に異なる2つの操作なので、非常に異なる機械語にコンパイルされます。JIT がこれを処理する方法は、複数のベースライン・スタブをコンパイルすることです。 コードの一部がモノモーフィックである (つまり、常に同じ型で呼び出される) 場合、それは 1 つのスタブを取得します。 それがポリモーフィックである場合 (あるコードから別のコードへと異なるタイプで呼び出されます) 、その操作を経たタイプの各組み合わせに対してスタブを取得します。
これは JIT はスタブを選ぶ前に多くの質問をする必要が有ることを意味します。
最適化コンパイラでは、関数全体が一緒にコンパイルされます。 型チェックは、ループの前に実施するように移動されます。
arr
がこれらの配列のいずれかである場合、JIT は arr
が整数かどうかを確認する必要はありません。これは、JIT がループに入る前にすべての型チェックを実行できることを意味します。結論
しかし、これらの改良によっても、JavaScript のパフォーマンスは予測できません。さらに高速化するために、JIT は実行時にいくつかのオーバーヘッドが増えています :
- 最適化と最適化解除
-
救済措置が起こったときのモニタの記録と復旧情報に使用されるメモリ
-
関数のベースラインバージョンと最適化バージョンを格納するために使用されるメモリ
次の記事では、アセンブリとコンパイラがどのように動作するかについて、さらに詳しく説明します。
AnonymousCoward :
T.Ukegawa :