V8 の最適化について 色々(主にVyacheslav Egorovさんの記事やスライド)読んでたのでそれのメモ。

この文章は2014年9月13日に書かれて最適化されていないため、この文章を元に最適化をすると失敗すると思います。


参考まとめ

V8に関するリソースまとめ

Hidden Class

V8がリリースされた時から特徴としてあげられている最適化

Hidden Classについては公式サイトにもDesign Elements - Chrome V8 — Google Developersというのがあるのでこれが分かりやすい。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

のようなコンストラクタで、これをnewしたインスタンスの形(プロパティの名前、種類とか)が同じなら、プロパティのアクセスが高速になるやつ。

逆に以下みたいなp2みたいに形が崩れると最適化がされなくなる(de-opt)

var p1 = new Point(10, 10);
var p2 = new Point(20, 20);
p2.name = "BREAK"; // <= これで最適化は壊れる

これは、Spidermonkeyでも同じようなものがありShape based polymorphic cachingというのが多分それだと思います。

実装的な話だとPolymorphic Inline Cache implementation of iv / lv5 - 枕を欹てて聴くとか。

このHidden Classとかは皆知ってると思いますが、実際にどういうケースで最適化が行われないかIRHydra2で確認するのが簡単で面白いです。

IRHydra2

mraleph/irhydra

IRHydraは、Chrome/V8のデバッグオプションでIR(中間表現)やインライン化等のログを元に最適化がされていない(de-opt)な部分を可視化したり、ディスアセンブリ(コードと並べて表示も出来る)、コントールフローの表示等をしてくれるツールです。 (Dart VMにも対応してます)

IRHydra2demo-1がまさにHidden Classの最適化されない事例についてのサンプルコードです。

var v2 = new Vec2(0.1, 0.2);
v2.name = "whatever";
loop(v2);

というようにv2でVec2の形が.nameによって変化するため、最適化が行われなくなっていることがIRHydra2では可視化されます。

Eager deoptimization

CheckMapsというところでpolymorphic(形が多様化)してることが検出されて最適化されてないというメッセージが見られます。

実際に途中で形が変わらないように最初からコンストラクタ関数にnameというのを追加してあげるとこのdeoptのメッセージは出なくなります。(これでいいのかな?)

function Vec2(x, y) {
  this._x = x;
  this._y = y;
  this.name = ""; // <= 追加
}

Vec2.prototype = {
  get x () { return this._x; },
  get y () { return this._y; },

  len2: function () {
    return this.x * this.x + this.y * this.y;
  },

  len: function () {
    return Math.sqrt(this.len2());
  }
}

function len(v) {
  // We are going to deoptimize here when we call
  // loop the second time because hidden class of
  // v2 does not match hidden class of v.
  // We changed by adding a new property "name" to
  // the object allocated with Vec2.
  return v.len();
}

function loop(v) {
  var sum = 0;
  for (var i = 0; i < 1e5; i++) sum += len(v);
  return sum;
}

var v = new Vec2(0.1, 0.2);
loop(v);

var v2 = new Vec2(0.1, 0.2);
v2.name = "whatever";
loop(v2);

実際に自分で書いたコードを試すには、v8_enable_disassembler=1でビルドしたV8が必要です。(ディスアセンブリが必要ない場合は、実行時のオプションだけでいいのかな)

自分は面倒だったのでbrew edit v8v8_enable_disassembler=1したものを作ってインストールしました。

使い方はIRHydra2に書いてありますが、v8で以下のようにオプションをつけて実行すると(Chromeもjs-flagで同じような感じ)、hydrogen-34272-1.cfgのようなのと、code.asmというファイルができます。

v8 example.js \
--trace-hydrogen \
--trace-phase=Z \
--trace-deopt \
--code-comments \
--hydrogen-track-positions \
--trace-inlining \
--redirect-code-traces \
--redirect-code-traces-to=code.asm

これをInlinedに読み込ませると表示してくれます。

img

v8(d8)の使い方やインストールは以下などを参考にした方がいいと思います。

最適化とループ

上記のdemo-1であえてループを回してる箇所があったのが疑問に思うかもしれません。

function loop(v) {
  var sum = 0;
  for (var i = 0; i < 1e5; i++) sum += len(v);
  return sum;
}

自分も最初ループなしで試したいコードを書いたりして以下のようなメッセージがでてcfgとasmファイルが生成されなかったりしてよくわかってなかったです。

Concurrent recompilation has been disabled for tracing.

これは、ループを回すことでV8側が最適化が必要なhot codeであると認識させるためにループを回してるんだと思います。

JVM HotSpotでは、ループの実行中に、 このループを内包した関数はhot codeだなと判断した場合、 実行中の関数をJITコンパイルします。

V8のJITコンパイラ、Crankshaftについて — V8 Crankshaft Overview 1.3 documentation という文章が分かりやすいです。

PyPy.js: Now faster than CPython ででてくるChromeはある程度実行回数が増えた時からパフォーマンスが安定して良くなるのはこれと同じ理由なのかな?


hot codeと最適化

先ほどループを回す(ある程度同じ処理を実行する)と最適化がかかる(JITコンパイルでコードをコンパイルするなど)というのは理にかなっていると思いますが、 このようなテストのためにループをするコードはどこかで見覚えがあると思います。

jsPerfを使ってマイクロベンチマークを取るときに、こういう無意味なループを含んだコードを書いたことがあるかもしれません。

何がいいたいかというと、こういうJavaScriptエンジン側の最適化によって取りたいベンチマークとは別の意味になってる可能性があるという話です。

まさにそういう話が以下の記事で書かれています。

bit-multiply · jsPerf のようにコード的には無意味なループを回してベンチマークを取ることがあると思います。

var a = Math.round(Math.random()*100),
    b = Math.round(Math.random()*100);

for(var i = 0; i < 10000; i++) {
   multiply(a,b);
}

The Black Cat of Microbenchmarks ではこのコードがV8でどのように最適化されていって、無意味なベンチになってるかをIRHydra2やEsprimaを使って解析していってます。

multiply() という計算を沢山するのを計測したいわけですが、この結果は結局使われていないため、このmultiply(a,b);という部分は無意味なコードとみなされてるかもしれません。

そのような無意味なコードは取り除かれる(Dead code elimination (DCE))可能性があります。(どれくらい行われるのか細かい話は分からない)

最適化を無効にする方法はありますが、ベンチマークを取りたい時は最適化自体は有効であってもいいと思うので、それが意図したもの以外も最適化されてるケースがあるかもというのが正しいベンチマークを取る難しさかも知れません。

意識できるレベルの最適化

ECMAScriptレベルのコードの最適化は実際のコードだと殆ど誤差になりがちで、DOM APIとかの方が圧倒的なのであまりコードを書くときにこういう最適化は気にしてないです。 (Hidden Classのやつぐらいは知っておいたほうがいいとは思います)

意識してやれるレベルのJavaScriptの最適化についてはSupersonic JavaScript // Speaker DeckJavaScript: Need for Speed // Speaker Deckあたりを見るのがいいと思います。

実際にこういうエンジンレベルの最適化を意識してるライブラリ例としてはBluebirdLo-DashReactなどがありますが、普通の人はこういう事やらないほうがいいと思います。

Bluebirdは特にこれを意識してて、fast caseに載せるために色々変則的なコードを書いたりしてて、Wikiにもそれらの事についてまとめられています。(ネイティブのPromiseより早いとかはこういうレベルの話があるからだと思います)

Lo-dashもそういうことのための変更を入れたり、ReactはV8側に最適化のためのコミットをしたりしています。

Chromium(V8も含む)プロジェクトの最適化周りの変更については以下のページにまとめられているそうです。

最初に書いたように自分で理解して、追えるレベルになってからこういう最適化を入れたほうがいいと思います。(自分は興味ないのでやらない)

JavaScriptが最適化されるコード書くより、JavaScriptが最適化されないコードを避ける方が方向としてコストに見合うものができるんじゃないかと思います。

この文章を元に最適化をすると失敗すると思います


その他

各JavaScriptエンジンのベンチを継続的に取ってるサイト

おわりに

Vyacheslav Egorovさんのブログを読んでて面白かったのでメモっただけの記事です。 最近だとthlorenz/v8-perfが広くまとまってるんじゃないかと思います。