V8の最適化とIRHydraでの可視化とベンチマークについてのメモ
V8 の最適化について 色々(主にVyacheslav Egorovさんの記事やスライド)読んでたのでそれのメモ。
この文章は2014年9月13日に書かれて最適化されていないため、この文章を元に最適化をすると失敗すると思います。
参考まとめ
V8に関するリソースまとめ
- V8 Resources - Vyacheslav Egorovさんによる
- thlorenz/v8-perf - Thorsten Lorenzさんによる
- Understanding V8 and JIT compilation basics - Google スライド - 概要分かりやすい
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
IRHydraは、Chrome/V8のデバッグオプションでIR(中間表現)やインライン化等のログを元に最適化がされていない(de-opt)な部分を可視化したり、ディスアセンブリ(コードと並べて表示も出来る)、コントールフローの表示等をしてくれるツールです。 (Dart VMにも対応してます)
- Release the IRHydra! v1
- (Pre)release IRHydra 2.0 v2- この動画でdemoを扱ってるので見ておくといいです。
IRHydra2のdemo-1がまさにHidden Classの最適化されない事例についてのサンプルコードです。
var v2 = new Vec2(0.1, 0.2);
v2.name = "whatever";
loop(v2);
というようにv2でVec2
の形が.name
によって変化するため、最適化が行われなくなっていることがIRHydra2では可視化されます。
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 v8
でv8_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に読み込ませると表示してくれます。
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))可能性があります。(どれくらい行われるのか細かい話は分からない)
- LICM(loop-invariant code motion)
- Dead code elimination (DCE)
- v8-perf/compiler.md at master · thlorenz/v8-perf
最適化を無効にする方法はありますが、ベンチマークを取りたい時は最適化自体は有効であってもいいと思うので、それが意図したもの以外も最適化されてるケースがあるかもというのが正しいベンチマークを取る難しさかも知れません。
意識できるレベルの最適化
ECMAScriptレベルのコードの最適化は実際のコードだと殆ど誤差になりがちで、DOM APIとかの方が圧倒的なのであまりコードを書くときにこういう最適化は気にしてないです。 (Hidden Classのやつぐらいは知っておいたほうがいいとは思います)
意識してやれるレベルのJavaScriptの最適化についてはSupersonic JavaScript // Speaker Deck、JavaScript: Need for Speed // Speaker Deckあたりを見るのがいいと思います。
実際にこういうエンジンレベルの最適化を意識してるライブラリ例としてはBluebird、Lo-Dash、Reactなどがありますが、普通の人はこういう事やらないほうがいいと思います。
Bluebirdは特にこれを意識してて、fast caseに載せるために色々変則的なコードを書いたりしてて、Wikiにもそれらの事についてまとめられています。(ネイティブのPromiseより早いとかはこういうレベルの話があるからだと思います)
Lo-dashもそういうことのための変更を入れたり、ReactはV8側に最適化のためのコミットをしたりしています。
Chromium(V8も含む)プロジェクトの最適化周りの変更については以下のページにまとめられているそうです。
最初に書いたように自分で理解して、追えるレベルになってからこういう最適化を入れたほうがいいと思います。(自分は興味ないのでやらない)
JavaScriptが最適化されるコード書くより、JavaScriptが最適化されないコードを避ける方が方向としてコストに見合うものができるんじゃないかと思います。
この文章を元に最適化をすると失敗すると思います
その他
各JavaScriptエンジンのベンチを継続的に取ってるサイト
おわりに
Vyacheslav Egorovさんのブログを読んでて面白かったのでメモっただけの記事です。 最近だとthlorenz/v8-perfが広くまとまってるんじゃないかと思います。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。