V8はどうやってJavaScriptコードを最適化しているのか?

  • このエントリーをはてなブックマークに追加

   

僕の過去記事で、NodeJSがなぜ速いかについて話した。今日は、V8について話したいと思う。

1-eiwHsFOQbAWWcifvC_zG7w

多分、これを読んでいる人の中には、JavaScriptの実行はC++と同じくらい速いと聞いたことがある人もいるかもしれない。そもそも、どうしてそれが可能になるのか理解できない人もいるだろう。C++がAhead-of-Time (AOT) コンパイルの静的型付け言語なのに対し、JavaScriptはJust-In-Time(JIT)コンパイラを搭載した動的型付け言語だ。そしてどういうわけか、最適化されたJavaScriptコードの実行速度はC++より若干遅いか、同じ速さですらある。

これがなぜなのかを理解するには、V8実装の基礎を知る必要がある。これは巨大なテーマなので、この記事ではV8の主な特徴を説明するだけに留めようと思う。隠しクラス、SSA、ICなどもっと詳しく知りたかったら…、僕の次の記事で紹介するつもりだ。

AST

全ては、JavaScriptコードとその抽象構文木(AST)から始まる。

コンピュータサイエンスにおける抽象構文木(AST)または構文木とは、プログラミング言語で書かれたソースコードの抽象統語構造の木表現である。その構文は「抽象的」であり、実際の構文に出てくる情報を細部まで表示しない。

コンピュータサイエンスにおける抽象構文木(AST)または構文木とは、プログラミング言語で書かれたソースコードの抽象統語構造の木表現である。その構文は「抽象的」であり、実際の構文に出てくる情報を細部まで表示しない。

Full-Codegenコンパイラ

このコンパイラの主な目的は、最適化なしでJavaScriptコードをネイティブコードにできる限り速くコンパイルすることだ。これはどんなケースでも対処してくれて、JavaScript関数の様々な場所におけるデータ型情報を集めた型フィードバック・コードを含んでいる。

ASTを伝ってノードを歩き、マクロアセンブラに直接命令を出す。このオペレーションの結果が一般的なネイティブコードだ。

以上。特別なことは何もない。ここでは最適化は行われず、複雑なケースのコードは実行時プロシージャへの命令により対処され、全てのローカル変数はヒープに記憶される…等々。

一番面白いのは、関数が「ホットに」なり最適化する時が来たとV8がみなした時だ。そうなると、Crankshaftコンパイラの出番だ。

Crankshaftコンパイラ

前述したとおり、full-codegenコンパイラは、関数の型フィードバック情報を集めてくれるコードとともに一般的なネイティブコードを作り出す。関数が「ホットに」なる時(ホットな関数とは、頻繁に呼び出される関数のこと)、CrankshaftはASTとその情報を使って、関数のための最適化コードをコンパイルすることができる。その後、最適化された関数はon-stack replacement (OSR)を用いて、最適化されていないものと置き替える。

しかし…最適化された関数は、全てのケースをカバーしているわけではない。もし型に不具合が起こったら、例えば、関数が整数の代わりに浮動小数点を返し始めたら、最適化された関数は脱最適化され、前の非最適化コードに置き換えられる。そんなの嫌ですよね?

例えば、2つの数字を加える関数があったとする。


const add = (a, b) => a + b;
// Let's say we have a lot of calls like this
add(5, 2);
// ...
add(10, 20);

この関数を整数のみで何度も呼び出すと、型フィードバック情報は、このaやbの引数は整数であるという情報になる。この情報とこの関数を使って、Crankshaftはこの関数を最適化できる。しかし、例えば下のような指示をしてしまうと、全て壊れてしまう。


add(2.5, 1); // float number as the first argument

Crankshaftは前の型フィードバック情報を踏まえ、この関数を通るのは整数のみだと想定しているのに、浮動小数点を渡そうとしている。このケースに対処する最適化コードはないため、単に最適化されていない状態に戻される。

あなたは、Crankshaftで一体どんなマジックが行われているんだ?と聞きたいかもしれない。ええと、Crankshaftコンパイラでは、連携する部分がいくつかある。

  • 型フィードバック(上述した通り)
  • Hydrogenコンパイラ
  • Lithiumコンパイラ

Hydrogenコンパイラ

Hydrogenコンパイラは、ASTと型フィードバック情報を入力データとして受け取る。この情報に基づき、Hydrogenは、静的単一代入形式(SSA)での制御フローグラフ(CFG)からなる高水準の中間表現(HIR)を生成する。

HIRを生成している間、定数畳み込みやメソッドのインライン化など、いくつかの最適化が適用される(…V8最適化のトリックについては次回の記事で話すつもりだ)。

これらの最適化の結果は、最適化された制御フローグラフ(CFG)であり、次のコンパイラ(実際のコードを生成するLithiumコンパイラ)への入力データとして利用される。

Lithiumコンパイラ

Lithiumコンパイラは最適化されたHIRを受け、特定マシンの低水準の中間表現(LIR)に翻訳する。LIRは、概念上マシン語に似ているが、それでも大部分はプラットフォーム独立型だ。

LIR生成の間もなお、Crankshaftはいくつかの低水準最適化を適用することができる。LIRが生成されると、Crankshaftは各Lithium命令に向けた一連のネイティブ命令を生成する。

その後、結果として生成されたネイティブ命令が実行される。

まとめ

これが、JavaScriptを最適化し、C++と同じくらい速く実行させるためにV8の内部で起こるマジックだ。それでも、下手に書かれたJavaScriptは最適化されず、最適化のメリットも得られないということは理解しておいてほしい。

次の記事で、V8最適化のトリック、コードのボトルネックを紐解く方法、脱最適化されたものの探し方、制御フローグラフ(CFG)の調査について話す予定だ。

原文:https://blog.ghaiklor.com/how-v8-optimises-javascript-code-a0f3bbd46ac9#(2017-1-18)
※元記事の筆者には直接翻訳の許可を頂いて、翻訳・公開しております。

 -Tech

FAworksではプロのコンサルタントが案件をお探しします

  関連記事

エンジニアの作業効率を一気に上げてくれる、無料Google Chrome拡張機能おすすめ20選

ちょっとした時短の積み重ねが作業時間を減らしてくれる 情報収集やブラウザチェック、ルーティーンワーク

node.js における stream の歴史とそれぞれの問題点

node.js における stream の歴史とそれぞれの問題点

内容 前史(stream API以前のstream) stream1 stream全盛期、ユーザラン

これから必ず伸びる!最低限抑えておきたい技術トレンド3つ(2015年度版)

※この記事は2015年度版です。最新の技術トレンド情報は下記の記事をご覧ください。 『「AIって何?

エンジニアとして実力を持って生きていくなら絶対取るべき資格

エンジニアとして何年か生きているのですが、そんな中で「これは受けて良かった! 勉強して良かった!」と

「AIって何?」なんて今さら聞けない!最低限抑えておきたいこれからの技術トレンド4つ(最新版)

【関連記事】 ❏これから必ず伸びる!最低限抑えておきたい技術トレンド3つ(2015年度版) ❏海外エ

Criteoにおける大規模機械学習の仕組み

Criteoの事業の核を担うのは、機械学習です。当社は、広告を表示させたいときの選択や、個別の製品レ

エンジニアがもっと働きやすい環境に!エンジニアに嬉しい福利厚生と導入企業まとめ

IT関連企業を筆頭に、今やどこの企業の求人を見ても「エンジニア募集中」の文字。優秀なエンジニアを獲得

基本的なシステム性能とOSジッタを計測するためのツールキット

Linux サーバの基本的なシステム性能とOSジッタを計測するためのツールキット

Jean Dagenaisは、mechanical-sympathyのスレッドで、Gil Teneの

なぜあなたのインターネットは遅いのか?問題を切り分けて特定するトラブルシューティング

はじめに 家庭、コーヒーショップ、またはオフィスでの作業中に「インターネット(インフラ)にまつわる問

継続的デリバリがもたらす効果と価値とは~ソフトウェア業界全体の「対応力を高める」トレンドを追え~

継続的デリバリがもたらす効果と価値 ~ソフトウェア業界全体のトレンド “React” を追え~

あなたが「リリース」という言葉を聞いた時、どのような感情が呼び起こされるだろうか?安堵?高揚感?ある