V8エンジンとJavaScriptの最適化のヒント
V8 は、JavaScriptをコンパイルするためのGoogleのエンジンです。 FirefoxにはSpiderMonkeyと呼ばれる独自のエンジンがあり、V8と非常によく似ていますが、違いがあります。 この記事では、V8エンジンについて説明します。
V8エンジンに関するいくつかの事実:
- C ++で記述され、ChromeとNode.js(および Microsoft Edge の最新リリース)で使用されます
- ECMA-262で指定されているようにECMAScriptを実装します
JavaScriptジャーニー
では、JavaScriptを送信してV8エンジンで解析すると、正確にはどうなりますか(これは、JavaScriptコードが縮小され、醜くなり、その他のクレイジーな処理が行われた後です)。
すべてのステップを示す次の図を作成しました。次に、各ステップについて詳しく説明します。
この記事では、JavaScriptコードがどのように解析されるか、およびJavaScriptを最適化コンパイラーにできるだけ多く取得する方法について説明します。 最適化コンパイラ(別名 Turbofan )は、JavaScriptコードを取得し、それを高性能のマシンコードに変換するため、提供できるコードが多いほど、アプリケーションは高速になります。 ちなみに、ChromeのインタプリタはIgnitionと呼ばれます。
JavaScriptの解析
したがって、JavaScriptコードの最初の処理は、それを解析することです。 構文解析とは何かについて正確に説明しましょう。
解析には次の2つのフェーズがあります。
- 熱心な(完全解析)-これは各行をすぐに解析します
- Lazy(pre-parse)-最低限のことを行い、必要なものを解析し、残りは後でまで残します
どちらが良いですか? それはすべて異なります。
いくつかのコードを見てみましょう。
// eager parse declarations right away
const a = 1;
const b = 2;
// lazily parse this as we don't need it right away
function add(a, b) {
return a + b;
}
// oh looks like we do need add so lets go back and parse it
add(a, b);
したがって、ここでは変数宣言はeager parsed
になりますが、関数はlazily parsed
になります。 これは、add
関数がすぐに必要になるため、add(a, b)
に到達するまでは素晴らしいので、eager parse
add
にすぐに到達できます。
eager parse
add
機能をすぐに実行するには、次の操作を実行できます。
// eager parse declarations right away
const a = 1;
const b = 2;
// eager parse this too
var add = (function(a, b) {
return a + b;
})();
// we can use this right away as we have eager parsed
// already
add(a, b);
これは、使用するほとんどのモジュールが作成される方法です。
関数のインライン化
Chromeは、基本的にJavaScriptを書き換えることがあります。その一例は、使用されている関数のインライン化です。
例として次のコードを取り上げましょう。
const square = (x) => { return x * x }
const callFunction100Times = (func) => {
for(let i = 0; i < 100; i++) {
// the func param will be called 100 times
func(2)
}
}
callFunction100Times(square)
上記のコードは、V8エンジンによって次のように最適化されます。
const square = (x) => { return x * x }
const callFunction100Times = (func) => {
for(let i = 100; i < 100; i++) {
// the function is inlined so we don't have
// to keep calling func
return x * x
}
}
callFunction100Times(square)
上記からわかるように、V8は基本的に、func
と呼ばれるステップを削除し、代わりにsquare
の本体をインライン化します。 これは、コードのパフォーマンスを向上させるので非常に便利です。
落とし穴をインライン化する機能
このアプローチにはちょっとした落とし穴があります。次のコード例を見てみましょう。
const square = (x) => { return x * x }
const cube = (x) => { return x * x * x }
const callFunction100Times = (func) => {
for(let i = 100; i < 100; i++) {
// the function is inlined so we don't have
// to keep calling func
func(2)
}
}
callFunction100Times(square)
callFunction100Times(cube)
したがって、今回はsquare
関数を100
回呼び出した後、cube
関数を100
回呼び出します。 cube
を呼び出す前に、square
関数本体をインライン化したため、最初にcallFunction100Times
を最適化解除する必要があります。 このような場合、square
関数は、cube
関数よりも高速であるように見えますが、最適化解除の手順により実行時間が長くなります。
オブジェクト
オブジェクトに関しては、内部のV8には、オブジェクトを区別するための型システムがあります。
単相性
オブジェクトは同じキーを持ち、違いはありません。
// mono example
const person = { name: 'John' }
const person2 = { name: 'Paul' }
ポリモーフィズム
オブジェクトは似たような構造を共有していますが、いくつかの小さな違いがあります。
// poly example
const person = { name: 'John' }
const person2 = { name: 'Paul', age: 27 }
メガモルフィズム
オブジェクトは完全に異なり、比較することはできません。
// mega example
const person = { name: 'John' }
const building = { rooms: ['cafe', 'meeting room A', 'meeting room B'], doors: 27 }
これで、V8のさまざまなオブジェクトがわかったので、V8がオブジェクトを最適化する方法を見てみましょう。
隠しクラス
隠しクラスは、V8がオブジェクトを識別する方法です。
これをいくつかのステップに分けてみましょう。
オブジェクトを宣言します:
const obj = { name: 'John'}
次に、V8はこのオブジェクトのclassId
を宣言します。
const objClassId = ['name', 1]
次に、オブジェクトは次のように作成されます。
const obj = {...objClassId, 'John'}
次に、オブジェクトのname
プロパティにアクセスすると、次のようになります。
obj.name
V8は次のルックアップを実行します。
obj[getProp(obj[0], name)]
これは、オブジェクトを作成するときにV8が実行するプロセスです。次に、オブジェクトを最適化してclassIds
を再利用する方法を見てみましょう。
オブジェクトを作成するためのヒント
可能であれば、コンストラクターでプロパティを宣言する必要があります。 これにより、オブジェクト構造が同じままになり、V8がオブジェクトを最適化できるようになります。
class Point {
constructor(x,y) {
this.x = x
this.y = y
}
}
const p1 = new Point(11, 22) // hidden classId created
const p2 = new Point(33, 44)
プロパティの順序を一定に保つ必要があります。次の例を見てください。
const obj = { a: 1 } // hidden class created
obj.b = 3
const obj2 = { b: 3 } // another hidden class created
obj2.a = 1
// this would be better
const obj = { a: 1 } // hidden class created
obj.b = 3
const obj2 = { a: 1 } // hidden class is reused
obj2.b = 3
一般的な最適化のヒント
それでは、JavaScriptコードをより適切に最適化するのに役立ついくつかの一般的なヒントを見てみましょう。
関数の引数の型を修正する
引数が関数に渡されるとき、それらが同じタイプであることが重要です。 引数のタイプが異なる場合、ターボファンは4回の試行後にJavaScriptの最適化の試行をあきらめます。
次の例を見てください。
function add(x,y) {
return x + y
}
add(1,2) // monomorphic
add('a', 'b') // polymorphic
add(true, false)
add({},{})
add([],[]) // megamorphic - at this stage, 4+ tries, no optimization will happen
もう1つのヒントは、グローバルスコープでクラスを宣言することを確認することです。
// don't do this
function createPoint(x, y) {
class Point {
constructor(x,y) {
this.x = x
this.y = y
}
}
// new point object created every time
return new Point(x,y)
}
function length(point) {
//...
}
結論
したがって、V8が内部でどのように機能するか、およびより最適化されたJavaScriptコードを作成する方法についていくつか学んだことを願っています。