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

  関連記事

1家に1人!旦那がエンジニアだと便利な5つのこと

よく便利なものに対して「1家に1台」とか言いますよね。 まさしくエンジニアはその「1家に1人」の便利

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

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

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

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

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

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

【企業別】Advent Calendar2014 はてブ数ランキングまとめ

皆さん、2014年も残す所1週間を切りましたが、いかがお過ごしでしょうか。もはや師走の風物詩となった

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

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

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

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

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

MySQL Cluster―MySQLはいかに2億QPSのスケーリングを実現したか

この記事は、オラクル社MySQL主任プロダクトマネージャ、アンドリュー・モルガン氏からのゲスト投稿で

エンジニアなら絶対ワクワクしちゃうコンピュータ映画7選

こんにちは!皆さん、映画観てますか?今回は人よりちょっぴり多く映画を観ていると勝手に自負している僕が

100万ppsを受信するプログラムを書くのはどのくらい難しいのか?【翻訳】CloudFlare ブログ

無料枠が充実していることでも人気なコンテンツデリバリネットワーク (CDN) を提供するCloudF