How to work JavaScript engine

このところ、JavaScriptがどのように実行されるのかについての記事をいくつか読んたので、備忘録と情報の整理を兼ねてブログに残したいと思います。間違っているところもあるかと思いますがご容赦ください。

プログラマが書いたプログラム(ソースコード)はそのままではただのテキストファイルです。コンピュータの頭脳であるCPUがソースコードを理解するためには、機械語に翻訳する必要があります。この機械語に翻訳することが JavaScript engine のお仕事になります。
JavaScript engine は

  1. トークンの作成
  2. 抽象構文木の作成
  3. 翻訳
  4. 実行

の4ステップでJavaScriptの実行を行います。

トークンの作成

HTMLを解析している途中で <script> タグを見つけると、ネットワーク、キャッシュ、またはサービスワーカーから、JavaScript ファイルのダウンロードを開始します。ダウンロードしたソースコードはバイトストリームとして、バイトストリームデコーダーに送信されます。そして、バイトストリームデコーダーはデコードされたバイトストリームからトークンを作成します。トークンとはプログラム的に意味のある文字列の最小単位のことです。例えば、 functionif, (, { などがトークンに該当します。そして、作成されたトークンはparserに送られます。

JavaScript engine には2種類のparserが使用されています。pre-parser と parser です。Webサイトの読み込み時間を減らすために、JavaScript engine はすぐに必要ではないコードは解析を後回しにします。 pre-parser は後で使用される可能性のあるコードを処理し、parser はすぐに必要なコードを処理します。例えば、ボタンをクリックしたときに何らかの処理が行われるというJavaScriptのコードが存在した場合、Webサイトを読み込んですぐに解析する必要はありません。ユーザーがクリックしたときに初めて、その処理が定義されているソースコードが parser に送信されます。

抽象構文木の作成

次に、先ほど作成されたトークンから、抽象構文木 ( Abstract Syntax Tree ) を作成します。JavaScriptの場合、 Abstract Syntax Tree、略して、ASTはJavaScriptオブジェクト ( JSON ) として表現されます。

初期のJavaScript engine (Netscape ブラウザで使用されていたような)は抽象構文木の作成、つまり、最適化が行われていませんでした。そのため、ステップ1のバイトコードの作成後、そのままインタープリタでコードを実行していました。
このようなバイトコードを作成し、インタープリタに送信することを目的としたコンパイラをベースラインコンパイラと呼びます。ベースラインコンパイラは最適化されていないバイトコードを作成するため、実行までの時間は短くなりますが、実行時間はとても遅くなります。つまり、Web上でのパフォーマンスは悪くなります。

翻訳

ASTを作成したら、ASTをもとにCPUが直接解釈できる機械語に変換していきます。

プログラミングでは、一般的に機械語に翻訳する方法が2つあります。

1つは「インタープリタ」と呼ばれる方式です。 インタープリタ方式は、ソースコードを直接実行する、または何らかの効率的な中間的なコード(中間表現)に、最初に全て変換して、あるいは、逐次変換しながら、解釈実行する方法のことです。 メリットとしては、コードを即時に実行して動作確認できるため、開発や修正をテンポよくすすめることができる点があります。デメリットとしては、同じコードを何度も実行するようなループ ( 繰り返し ) の箇所などでは、一度構文解釈した部分でも毎回最初から解釈と実行を行うので、実行速度が遅くなるという点があります。

2つ目は「コンパイル」と呼ばれる方式です。 インタープリタ方式との違いは、ソースコードの翻訳だけを行い、実行を伴わない点です。 これにより、ループ内のコードは、そのループを通過するたびに変換を繰り返す必要がないため、実行速度が速くなるといったメリットがあります。反対に、最初にコンパイルの手順を踏む必要があるため、実行開始をするまでに時間がかかるというデメリットがあります。

インタープリタがループを通過するたびにコードを再翻訳し続けなければならないというインタプリタの非効率性を取り除く方法として、よりよい方法を考える必要がありました。

初期のGoogleChromeで使用しているV8と呼ばれる JavaScript engine を使用しています。このエンジンは、JavaScriptのパフォーマンスを向上させるため、2つのピースを追加しました。full-codegen と呼ばれるベースラインコンパイラと crankshaft と呼ばれる最適化コンパイラです。

1つ目の full-codegen の仕事は、アプリケーションの実行を可能な限り早くするために最適化されていないバイナリコードを吐き出すことです。
2つ目の crankshaft はアプリケーションの実行中に起動し、最適化されていないバイナリコード(full-codegenによって吐き出された)の1部を最適化して置き換えます。これにより最適化されたバイナリコードを生成することができ、アプリケーションのパフォーマンスを向上させることができるようになりました。

上記のような JavaScript engine にはインタープリタが含まれていません。このようなモデルを JIT(Just-In-Time)コンパイルと呼びます。コードはすぐにマシンレベル(CPUが理解できるレベル)のバイナリコードにコンパイルされ、必要な部分は後に最適化されます。

それから2017年に新しいJavaScript engine としてV8の新しいバージョンがリリースされました。
この新しい V8 は新しいインタープリタパイプラインである Ignition を導入しました。これは先程説明した初期のV8と異なり、インタープリタを活用します。 Ignition はベースラインコンパイラを使用して、バイトコードを生成し、それをインタープリタでバイナリコードに変換するという一連の流れのことを呼びます。  

次に、新しい最適化コンパイラとして、TurboFan を導入しました。TurboFan は Ignition インタープリタによって生成されたバイトコードをバックグランドで最適化し、バイナリコードを生成します。  

この TurboFan の仕組みを説明すると、インタープリタによって翻訳されたコードで同じコード行が数回実行される場合、そのコードセグメントは"ウォーム"と呼ばれるようになります。そして、それがたくさん実行されると"ホット"と呼ばれるようになります。複数回実行されるような "ホット”コードは TurboFan によって型の判定をしなくても良くなるような処理が内部的に行われ、それによって、より最適化されたコードになります。JavaScriptは動的型付け言語なので、データの種類は絶えず変化する可能性があります。そのため、JavaScriptエンジンが、特定の値を持つデータ型を毎回チェックする必要がある場合は、非常に遅くなります。

コードの翻訳にかかる時間を短縮するために、最適化されたバイナリコードは、同じデータ型を返す特定のコードを繰り返し使用した場合、最適化されたバイナリコードを再利用します。そうすることで処理を高速化することができます。しかし、JavaScriptは動的型付け言語なので、同じコードでも異なるデータ型を返すことがあります。その場合、最適化されたバイナリコードは使用されず、ベースラインコンパイラが生成したバイトコードを使用して、インタープリタがバイナリコードを生成します。

ここで気づいた方は多いかもしれませんが、上記のような仕様があるため、この新しいJITコンパイラ(こちらもJITコンパイルの一種のようです)はTypeScriptと非常に相性が良いです。

実行

最終的に機械語であるバイナリコードに変換されたJavaScriptコードは、CPUで実行されます(やったー!!)

まとめ

最近フロントエンドのパフォーマンス関連に興味があり、こういった記事をちょこちょこ読んでいます。まだまだ勉強不足でこの勉強したことが実務ででのように役立つのかはわかりませんが、いつか役に立つ時が来ると信じて勉強しています。フロントエンドを知る上でブラウザのことはよく知らなければいけない部分かと思います。少しづつでも知識を深めていきたいです。

参考文献


関連記事

この記事のハッシュタグに関連する記事が見つかりませんでした。

最新記事

カテゴリー

アーカイブ

ハッシュタグ