過去および将来のJavaScriptコンパイル

現在、ブラウザでのjsコードの高速実行は当たり前のことと考えており、毎日、JSを使用して実装できるものの刺激的な例が増えています。 しかし、これは常にそうではありませんでした。 この記事では、ブラウザーでのコードのコンパイルを担当するJSエンジン、加速の歴史的パス、および可能な将来のパスについて説明します。



jsコードを解釈する最初のエンジンは、SpiderMonkeyでした。これは、1995年にNetscape 2.0ブラウザに導入されました。その迅速な作成の神話は、十分に文書化されてます 。 ブレンダン・アイクは言語を設計し、コンパイラーを構築するのにたった10日しかありませんでした。 Javascriptは最初から成功しており、同じコードの8月までに、MicrosoftはすでにJScriptのバージョンをInternet Explorer 3.0に統合していました。 1996年末までに、この言語は正式な標準化委員会に受け入れられ、翌年の6月に公式の標準ECMA-262を取得しました。 それ以来、JSのサポートはすべてのブラウザで必須になり、すべての主要メーカーはJSをサポートする独自のエンジンの構築を開始しました。 長年にわたり、これらのエンジンは開発され、相互に置き換えられ、名前が変更され、次のエンジンの基礎になりました。 作成されたすべてのバージョンを追跡することは、気弱な人のためではありません。



画像



たとえば、KDEのオープンソースKJSエンジンを使用したKonquererブラウザを覚えている人はほとんどいません。 その後、Apple開発者はこのプロジェクトを「分岐」し、WebKitを将来のカーネルに開発し、進化の過程でいくつかの名前を変更しました: SquirrelfishSquirrelfish ExtremeNitro



反対のプロセスも行われました。 名前が変更されていないエンジンがありますが、すべての内部は変更されています。 たとえば、MozillaのSpiderMonkeyには、1995年に存在したコードのヒントはありません。



2000年代半ばまでに、JavaScriptは標準化され、非常に一般的になりましたが、その実行は依然として遅かったです。 速度の競争は、多くの新しいエンジンが登場した2008年に始まりました。 2008年初頭、Operaの最速エンジンはFutharkでした。 夏までに、MozillaはTracemonkeyを導入し、Googleは新しいV8 JavaScriptエンジンを搭載したChromeをリリースしました。 豊富な名前にもかかわらず、彼らは皆同じ​​ことをしようとし、各プロジェクトは実行速度が有利に異なることを望んでいました。 2008年以降、エンジンは設計の最適化により改善されており、主要なプレーヤー間で最速のブラウザーを構築する競争がありました。



JavaScriptエンジンについて話すとき、私たちは通常コンパイラを意味し、コンパイラによって生成されたコードをより高速にすることが本当の挑戦です。 おそらく、すべてのJSライターがコンパイラーの動作について困惑しているわけではありません。

JavaScriptは高水準言語であると理解されています。 これは、読みやすく、柔軟性が高いことを意味します。 コンパイラの仕事は、この人間が読み取れるコードからネイティブコードを生成することです。



通常、コンパイルは4段階で行われます。



1.字句解析(スキャン)の段階で、コンパイラはソースコードをスキャンし、トークンと呼ばれる個別のコンポーネントに分割します。 これは通常、正規表現によって実現されます。

2.パーサーは、処理されたコードを構文ツリーに構造化します。

3.次に、この構造はトランスレーターによってバイトコードに変換されます。 最も単純な場合、変換は、トークンをプレゼンテーションバイトコードにマッピングすることで表すことができます。

4.最後に、バイトコードはネイティブコードを取得するためにバイトコードインタープリターを通過します。



これは古典的なコンパイラ設計であり、長年使用されています。 しかし、デスクトップアプリケーションの要件はブラウザの要件とは非常に異なり、このアーキテクチャは多くの方向で困難に直面し始めています。 これらの矛盾の解消は、ブラウザ間の速度をめぐる競争の結果でした。



速く、エレガント、正しい


JavaScriptは非常に柔軟な言語であり、不正な構成要素に対して非常に寛容です。 遅延バインディングを使用して、型指定の弱い動的言語用のコンパイラを作成する方法は? 速くする前に、きちんとしたものにしなければなりません。 ブレンダン・アイクが言ったように、

「速く、エレガントで、正しい。 「正しい」が既に選択されている場合、2を選択します”

「高速、スリム、正しい。 1つが「正しい」限り、任意の2つを選択してください”



MozillaのJesse Rudermanは、コンパイラの正確性をテストするための非常に便利なjsfunfuzzツールを作成しました。 Brendanは、JavaScriptコンパイラのパロディと呼びました。その目的は、検証のためにコンパイラに送信される最も奇妙で有効なコンストラクトを作成することだからです。 このツールにより、多数の極端なケースとバグを特定できました。



Jitコンパイラー


古典的なアーキテクチャの主な問題は、実行時のバイトコードの解釈がかなり遅いことです。 バイトコードコンパイル手順をマシンコードに完了することでパフォーマンスを向上させることができますが、これにはWebページの読み込み時にユーザーからの長い待ち時間が必要になります。



解決策は、「遅延コンパイル」またはオンザフライのコンパイルです。 名前が示すように、コードは必要なときにマシンコードにコンパイルされます。 JITコンパイラは、さまざまな最適化戦略を備えたさまざまなテクノロジで登場しています。 個々のコマンドを最適化するために調整されたものもあれば、ループや関数などの反復操作を最適化するためのものもあります。 最新のJavaScriptエンジンは、コードのパフォーマンスを向上させるために連携するこれらのコンパイラーのいくつかを使用します。



JavaScript JITコンパイラー


最初のJavaScript JITコンパイラは、MozillaのTraceMonkeyでした。 これは、最も繰り返されるループを追跡するため、いわゆるtraceroute JITでした。 これらの「ホットループ」は、マシンコードにコンパイルされます。 この最適化のみのおかげで、Mozillaは以前のエンジンに比べて20%から40%のパフォーマンスの改善を得ました。



TraceMonkeyを起動してまもなく、Googleは新しいV8エンジンとともにChromeをリリースしました。 V8は、速度の最適化のために特別に設計されました。 主なアーキテクチャソリューションは、バイトコード生成を拒否することでした。代わりに、トランスレータが直接ネイティブコードを生成します。 開始から1年以内に、チームはレジスタ割り当てを適用し、インラインキャッシングを改善し、正規表現エンジンを完全に書き換えて、10倍高速化しました。 これにより、JavaScriptの実行速度が150%向上しました。 スピード競争は完全に続きました!



後に、ブラウザメーカーは、追加のステップを備えたコンパイラを導入しました。 制御フローグラフまたは構文ツリーが生成された後、コンパイラは、マシンコードを生成する前に、このデータを使用してさらに最適化できます。 そのようなコンパイラの例は、 IonMonkeyCrankshaftです。



これらのすべての変換の野心的な目標は、ネイティブCの速度でJavaScriptコードを実行することです。数年前、この目標は信じられないほどでしたが、実行速度の差は狭まりつつあります。



次に、JSコンパイルのいくつかのプライベート機能について説明します。



非表示のクラス


JavaScriptでオブジェクトと構造を構築することは開発者にとって非常に簡単であるため、これらの緩やかに決定された構造をナビゲートすることは、コンパイラにとって非常に遅くなります。 たとえば、Cでは、プロパティを保存してプロパティにアクセスする一般的な方法は、ハッシュテーブルを使用することです。 ハッシュテーブルの問題は、非常に大きなハッシュテーブルの検索が非常に遅くなる可能性があることです。

このプロセスを高速化するために、V8とSpiderMonkeyはどちらもJavaScriptオブジェクトの内部表現である隠しクラスを使用します。 それらはGoogleではマップと呼ばれ、Mozillaではシェイプされますが、本質的には同じものです。 これらの構造は、標準の辞書検索よりも検索がはるかに高速です。



型推論


Javascriptの動的型付けにより、同じプロパティをある場所では数字に、別の場所では文字列にすることができます。 残念ながら、この多様性により、コンパイラで多数の追加型チェックが行われ、条件付きチェック付きのコードは、変数型に対して定義されたコードよりもはるかに長く、遅くなります。



解決策は、すべての最新のJSコンパイラが持っている型推論方法です。 コンパイラーは、これらのプロパティーのタイプについて想定します。 ステートメントが正しい場合、これらのセクションの高速マシンコードを生成する型付きJITに実行を渡します。 タイプが一致しない場合、コードは型指定されていないJITに渡され、条件チェック付きのより遅いコードで実行されます。



インラインキャッシング


これは、最新のJavaScriptコンパイラで最も一般的な最適化です。 これは新しい技術ではありません(30年前にSmalltalkコンパイラに最初に適用されました)が、非常に便利です。



インラインキャッシングでは、型推論と隠しクラスの両方の以前の手法が機能する必要があります。 コンパイラは、新しいクラスを検出すると、定義されているすべての型とともに、隠されたクラスをキャッシュします。 この構造が後で発生した場合、保存されているキャッシュとすばやく比較できます。 構造またはデータ型が変更された場合、それらはより低速な汎用コードに転送されるか、一部のコンパイラは多態性インラインキャッシングを実行し、データ型ごとに1つの構造の個別のキャッシュを生成します。 詳細については、V8チームのVyacheslav Egorovによる記事をご覧ください。

コンパイラがコード構造とデータ型を取得すると、さまざまな追加の最適化が可能になります。 以下に例を示します。



インライン展開、または「インライン化」


関数呼び出しは、何らかの検索を必要とし、検索が遅くなる可能性があるため、高価な操作です。 アイデアは、関数の本体コードを呼び出される場所に置くことです。 これにより、不必要な分岐が回避され、実行速度が向上しますが、実行可能コードのサイズが大きくなります。



サイクルの不変の変化、「上昇」


ループは最適化の最初の候補です。 ループから不要な計算を削除することにより、パフォーマンスを大幅に改善できます。 最も単純な例:配列要素のforループ。 各反復で配列の長さを計算することは有益ではないため、この操作は実行され、サイクルごとに「上昇」します。



定数の畳み込み


不変変数を含む式と同様に、定数式が計算されます。



共通の部分式を削除する


定数と同様に、コンパイラーは同じ計算を含む式を検索します。 これらの式は、計算値を持つ変数に置き換えることができます。



デッドコード除去


使用されていないか、到達できないコード。 使用されたことがない関数の本体を最適化することは意味がなく、単に削除することができます。



これは、ブラウザメーカーが野心的な目標を達成するために取り組んでいる方向を示す単純なツールの小さなセットです。 それらの多くは、オペレーティングシステムとしてのWebの概念に長期的な投資を行いました。 これを実現するために、彼らはネイティブCの速度でJavaScriptコードを実行し、ネイティブアプリケーションとWebアプリケーションの違いを徐々に消去するタスクを設定しました。



ES.next



EcmaScript仕様の次のバージョン(EcmaScript 6)は長い間運用されており、今年の最終バージョンが期待されています。 プロジェクトの目標の1つは、迅速なコンパイルです。 型付け、バイナリデータ、型付き配列など、実現可能なツールのセットについて説明します。 入力したコードを直接JITに送信して、コンパイルと実行時間を短縮できます。



ブラウザーによるES.nextのコンテンツはまだかなり制限されていますが、少なくともここでそれに従うことができます。また、JavaScriptで記述されたES.next JavaScriptコンパイラーであるTraceurを使用してテストを開始することもできます。



Webgl




ブラウザのJavaScriptは、DOMの操作に限定されません。 多数の最新のブラウザゲームは、標準の2Dコンテキストを使用してページのキャンバス要素に直接レンダリングされます。 キャンバスでレンダリングする最速の方法はWebGLです。WebGLは、高価な操作をGPUに移植し、CPUをアプリケーションロジックに残して最適化を提供するAPIです。



何らかの形式の WebGL は、ほとんどのブラウザー (主にChromeおよびFirefox)でサポートされています 。 SafariおよびOperaユーザーは、最初に適切なオプションを有効にする必要があります。 マイクロソフトは最近、IE11でのWebGLのサポートも発表しました。



残念ながら、完全なブラウザサポートを備えていても、最新のGPUドライバーに依存しているため、WebGLがすべてのユーザーに等しく機能することを保証することはできません。 Google Chromeは、これらのドライバーがインストールされていない場合に代替ソリューションを提供する唯一のブラウザーです。 WebGLは非常に強力なテクノロジーですが、その最高点はまだ到達していません。 セキュリティの問題に加えて、モバイルデバイスのサポートは非​​常に異種です。 そして、もちろん、古いブラウザではサポートされていません。



コンパイルの結果としてのJavascript


すべての最新のWebアプリケーションはクライアントでJavaScriptを使用していますが、それらのすべてがJavaScriptで記述されているわけではありません。 多くは完全に異なる言語(Java、C ++、C#)で記述され、JSでコンパイルされます。 TypeScriptなど、開発を容易にするためにJavaScriptを拡張する言語として作成されたものもあります。



ただし、クロスコンパイルには問題があります。 縮小されたコードは判読できず、デバッグが困難です。実際には、ソースコードでマッピングを保存する中間ファイルであるソースマッピングをサポートしているブラウザーでのみ可能です。

数年前、MicrosoftのScott Hanselmanは、 JavascriptがWebのコンパイル言語であるという仮説を提唱しました 。 最新の縮小されたJavaScriptアプリケーションは読みにくいという彼の発言は論争しにくいですが、それでも彼の投稿は多くの議論を引き起こしました。 多くのWeb開発者は、単にブラウザーでソースコードを調べることから始めましたが、現在ではほとんど常に難読化されています。 これらの理由から、将来の開発者の一部を失うことはできますか?



このアイデアの興味深いデモンストレーションは、JavaScriptでLLVMバイトコードをコンパイルできるEmscriptenプロジェクトでした。 LLVM (低レベル仮想マシン)は非常に人気のある中間コンパイル形式であり、ほぼすべての言語用のLLVMコンパイラを見つけることができます。 このアプローチにより、誰もが自分に都合の良い言語でソースコードを書くことができます。 プロジェクトはまだ初期段階ですが、チームはすでに多くの印象的なデモをリリースしています。 たとえば、Epic開発者は、LLVM CコンパイラとEmscriptenを使用してasm.jsコードにコンパイルすることにより、Unreal Engine 3をJavaScriptとWebGLに移植しました。



asm.js


同じ方向で機能するプロジェクト。 その作成者は、JavaScriptを言語の非常に限られたサブセットのアセンブラーとして、「機械語としてのjavascript」の呼び出しを文字通り受け入れました。 したがって、理論的には、asm.jsコードを手で書くことができますが、誰も望んでいません。 この機能を最大限に活用するには、2つのコンパイラが必要です。

Emscriptenコンパイラーはasm.jsを生成できます。 結果のJavaScriptは判読できませんが、正確で下位互換性があります。 ブラウザーエンジンがasm.js形式を認識し、このコードを別のコンパイラーに渡すと、非常に高速になります。 この目的のために、MozillaはOdinMonkeyに取り組んでいます。これは、IonMonkeyに組み込まれているasm.jsコンパイラを最適化します。 Googleはまた、Chromeでのasm.jsのサポートを発表しました。 予備テストでは、コンパイルされたC ++の約50%のパフォーマンスが示されましたが、これは驚異的な成果であり、速度はJavaおよびC#に匹敵します。 チームは、結果が改善されると確信しています。

Mozilla Researchは今まさに波の頂点にあります。 Emscriptenとasm.jsに加えて、 LLJSプロジェクト(JavaScript as C)もあります。また、Intelと一緒にRiver Trail-並列計算用のECMAScript拡張機能を開発しています。 この方向にどれだけの労力が注がれているか、そしてすでにどのような結果が得られているかを考えると、ネイティブの速度でJavaScriptを実行することは、以前のように達成不可能ではないと想定できます。



ORBX.js


完全な仮想化を通じてJavaScriptのパフォーマンスの問題を解決することを提案する人もいます。 そのマシンでアプリケーションを実行する代わりに、クラウドで実行されます。 もちろん、これはJSコンパイルの問題自体の解決策ではなく、ユーザーの代替解決策です。 ORBX.jsは、JavaScriptのみを使用して1080ピクセルの解像度でビデオをストリーミングできるビデオコーデックの実装です。 MozillaとOtoyの共同プロジェクト。

テクノロジーはもちろん印象的ですが、おそらくそれは解決するよりも多くの問題を生み出します。



Javascriptの今後のコンパイルについてどう思いますか?



All Articles