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ではプロのコンサルタントが案件をお探しします

  関連記事

NGINXのスレッドプールがパフォーマンスを9倍にする!

NGINXのパフォーマンスをスレッドプールで9倍にする

はじめに NGINXが接続処理に非同期かつイベント駆動のアプローチを用いていることは、よく知られてい

Twitterはどうやって1秒に3,000もの画像を処理しているのか

Twitterはどうやって1秒に3,000もの画像を処理しているのか

現在、Twitter は1秒間あたり3,000枚の画像(約200GB)を作成し持続している。 しかし

パフォーマンスの悪さがeコマースの売上に与える影響とは

休暇期間になると、ECサイトが遅いせいでショッピングカートの18%が破棄されるという。そんな恐ろしい

【比較表あり】非エンジニアの人にも知ってほしい。エンジニアに優しいチャット・コミュニケーションツールまとめ

エンジニアに合ったコミュニケーションツール プロジェクトを円滑に進行させるためにも、チームでのコミュ

Googleはあなたのスマホをいつでもハックできる!?

Googleはあなたのスマホをいつでもハックできる!?

by Don Hankins スマホの位置情報オフにしてますか? FacebookやTwitterに

今、なぜフルスタックエンジニアになる必要が?

by Jim Pennucci 既にバズワードにもなりつつありますが、今、まさに現在進行中で、フルス

React/Fluxにおける問題とReducerが切り開く道

私がReact/Fluxアプリケーションを書いてきて、もう1年になる。Flux開発の1年を振り返って

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

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

Dockerコンテナとイメージの仕組みを視覚化してみた

この記事は、Docker 102レベルを意図して書かれている。Dockerが何か分からない、または仮

Dockerコンテナのためのテスト戦略

Dockerコンテナのためのテスト戦略

おめでとう!あなたはDockerイメージの作り方を知っていて、わかりやすいアプリケーションで複数のコ