V8エンジンとJavaScriptの最適化のヒント

提供:Dev Guides
移動先:案内検索

V8 は、JavaScriptをコンパイルするためのGoogleのエンジンです。 FirefoxにはSpiderMonkeyと呼ばれる独自のエンジンがあり、V8と非常によく似ていますが、違いがあります。 この記事では、V8エンジンについて説明します。

V8エンジンに関するいくつかの事実:

  • C ++で記述され、ChromeとNode.js(および Microsoft Edge の最新リリース)で使用されます
  • ECMA-262で指定されているようにECMAScriptを実装します

JavaScriptジャーニー

では、JavaScriptを送信してV8エンジンで解析すると、正確にはどうなりますか(これは、JavaScriptが縮小され、醜くなり、JavaScriptコードに対して他のクレイジーな処理が行われた後です)。

すべてのステップを示す次の図を作成しました。次に、各ステップについて詳しく説明します。

この記事では、JavaScriptコードがどのように解析されるか、およびJavaScriptを最適化コンパイラーにできるだけ多く取得する方法について説明します。 最適化コンパイラ(別名 Turbofan )は、JavaScriptコードを取得し、それを高性能のマシンコードに変換するため、提供できるコードが多いほど、アプリケーションは高速になります。 ちなみに、ChromeのインタプリタはIgnitionと呼ばれます。

JavaScriptの解析

したがって、JavaScriptコードの最初の処理は、それを解析することです。 構文解析とは何かについて正確に説明しましょう。

解析には次の2つのフェーズがあります。

  • 熱心な(完全解析)-これは各行をすぐに解析します
  • レイジー(事前解析)-最低限のことを行い、必要なものを解析し、残りを後でまで残します

どちらが良いですか? それはすべて異なります。

いくつかのコードを見てみましょう。

// 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 parseaddにすぐに到達できます。

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コードを作成する方法についていくつか学んだことを願っています。