Webアプリケーションのパフォーマンス最適化には様々な観点や手法があります。 Deopt Explorer では、普段あまり気にされることのない V8(JavaScript ランタイム)の挙動に関心を置いたパフォーマンス改善の種を見つけることができます。 この資料では、Deopt Explorer を使って Web アプリケーションのパフォーマンスを改善するための事前知識および手法について紹介しています。
Deopt ExplorerでWebアプリのパフォーマンスを改善しよう!in Cybozu Frontrend Day 2023/06/30BaHo @b4h0_c4t1
View Slide
自己紹介- 名前- BaHo- 所属- フロントエンドエキスパートチーム- 興味関心- Webフロントエンド- ゲーム- JRPG とかアクション系が好き2
Deopt Explorer って何?V8 のトレースログをもとにインラインキャッシングに適していないコードを発見・可視化してくれる VS Code 拡張です。※V8 : Chrome や Node.js で使われている JavaScript エンジン3
🤔4
インラインキャッシングって何だよ以前に実行したコードのキャッシュを利用して次回以降の実行速度を高速化する手法5const sum = (a, b) => {return a + b;};sum(1, 2); // 3sum(2, 3); // 5JavaScript ランタイムでは、コードを実行する際に対象コードを実行可能なバイナリに変換してメモリへ展開し、それを実行します。左の例では、sum(1, 2); を実行した際にメモリへ展開された sum() 関数を流用して sum(2, 3) の処理が実行されるため、 sum(1, 2) と比較して sum(2, 3) の実行は高速になります。※「implicit any だ!○せ!」と思ったそこのあなた、これは JavaScriptです。
インラインキャッシングができるとき・できないとき実行バイナリ上で同様の計算処理が走るコードはできる6const sum = (a, b) => {return a + b;};sum(1, 2); // 3sum(2, 3); // 5 IC!const sum = (a, b) => {return a + b;};sum(1, 2); // 3sum(2, "3"); // "23" not IC!どちらも算術的な加算を実行しているのでインラインキャッシングが有効sum(1, 2) は算術的な加算だがsum(2, “3”) は文字列結合のため、インラインキャッシングは無効普段 TypeScript を利用しているのならプリミティブな値でこの現象を踏む人は少ないと思います
Q. TS でインラインキャッシングできないことあるの?A. ありまぁす!主にオブジェクトを取り扱うときに発生します。Q. TSならオブジェクトにも型をつけられるよね?実は JavaScript ランタイムは内部でオブジェクトの型を保持していて、TSのそれとはいくつかの不整合があります。7※string | number とか Optional みたいな型定義でも発生するので実際は割とどこでも発生します
Hidden ClassJavaScript ランタイムが持つ、任意のオブジェクト型(のようなもの)プログラムを実行しながら動的に生成されます。特定の関数に対しての引数となるオブジェクトのプロパティとその型およびレイアウト(オフセット)を保持しています。このオフセットのおかげでオブジェクトプロパティへのアクセスが高速化されています。が、今回とは別のお話なので割愛8※V8 の実装では Map と呼ばれています。
Hidden Class の生成プログラム内で関数が呼び出されると、呼び出された引数をもとに Hidden Classが生成されます。次回以降の呼び出しは、生成された Hidden Class をもとにインラインキャッシシングが可能か検査が行われます。9const sum = (x) => {return x.a + x.b;};sum({ a: 1, b: 2 }); // 3sum({ a: 2, b: 3 }); // 5map 0x38a5002079c9 extendsObject {a: unknown;b: unknown;}
Hidden Class の追加引数オブジェクトの形が変わると、そのオブジェクトに対する Hidden Class の追加が行われます。10const sum = (x) => {if (x.c) return x.a + x.b + x.c;return x.a + x.b;};sum({ a: 1, b: 2 }); // 3 HiddlenClass1sum({ a: 1, b: 2, c: 3 });map HiddenClass1 extends Object {a: unknown;b: unknown;}map HiddenClass2 extends Object {a: unknown;b: unknown;c: unknown; // Added}
Hidden Class のオフセットオブジェクトプロパティの定義順が変わっても新しい Hidden Class が生成されます。11const sum = (x) => {return x.a + x.b;};sum({ a: 1, b: 2 }); // 3 HiddlenClass1sum({ b: 1, a: 2 }); // 3 HiddenClass2map HiddenClass1 extends Object {a: unknown;b: unknown;}map HiddenClass2 extends Object {b: unknown;a: unknown;}
つまり...TSでの型定義に関わらず、Hidden Class が変わると (内部の型が変わるため) インラインキャッシングができない!となりますちなみに、任意の引数オブジェクトに対して Hidden Class が複数存在している状態(性質)をPolymorphic、その数がより多いものを Megamorphic と呼びます。12※ポリモーフィックです。どこかできいたことがありますね。
Deopt Explorer の話に戻ります13
Deopt Explorer って何?V8 のトレースログをもとにインラインキャッシングに適していないコードを発見・可視化してくれる VS Code 拡張です。※V8 : Chrome や Node.js で使われている JavaScript エンジン14
🤓💡15
何を言っているか分かりましたね?(圧)16
ここまで前座17
実際に Explore してみよう!- 必要なもの- お手持ちの PC- VS Code (Market Place から Deopt Explrer を追加しておこう)- Google Chrome (MS Edge でも良いらしいけど未検証 )- 手順- Chrome で検証したいWebアプリのトレースログを取得する- 取得した v8-log を Deopt Explorer で読み込む18chrome --no-sandbox--js-flags=--log-deopt,--log-ic,--log-maps,--log-maps-details,--log-internal-timer-events,--prof,... <トレースしたいURL>alias chrome="/path/to/Google\ Chrome"
以上!19
とても簡単なデモ20
プログラムの解説21Document>function f(x, y) {if (x <= y) {return { x, y };} else {return { y, x };}}function g(p) {const x = p.x; // polymorphic}for (let i = 0; i < 1000; i++) g(f(0, 1));g(f(1, 0));index.html polymorphicExample.js関数 f() の引数が {x, y} か {y, x} として呼び出されているだけのページ
Deopt Explorerにトレースログを流した結果左の「ICs」にpolymorphicExample.js がある!「Polymorphic」の注釈がついていますね。22
強調箇所をピークしてみるUninitialized => Monomorphic x => Polymorphic x と Hidden Class が遷移していることがわかります。また、それぞれに対応した Map も参照されていますね。23
Map を覗く1つ目(左)が {x, y} なのに対して、2つ目が {y, x} になっているのがわかります。その Mapがどこで追加されたのかも Added by *** から追跡できます。24
プログラムを修正する修正箇所がわかったので早速直しましょう25function f(x, y) {if (x <= y) {return { x, y };} else {return { y, x };}}function g(p) {const x = p.x; // polymorphic}for (let i = 0; i < 1000; i++) g(f(0, 1));g(f(1, 0));polymorphicExample.jsfunction f(x, y) {return { x, y };}function g(p) {const x = p.x; // polymorphic}for (let i = 0; i < 1000; i++) g(f(0, 1));g(f(1, 0));polymorphicExample.js※身も蓋も無い修正
トレードオフ何でもかんでも引数を固定すれば良いわけではない- Optional な引数を辞めるために 引数ありなしの2パターンの関数を作ってしまうとその分メモリが圧迫されます。- Webアプリにとってはオブジェクトへのアクセスよりも描画にかかるコストの方が圧倒的に高いため、どちらかを取らなければいけない場面では描画側の懸念事項を優先した方が良い26要はバランスとはいえ、この観点から修正できるプログラムは多いですし、積極的にリファクタリングしてみてください
意外と知られていないパフォーマンスチューニング実は引数に気を使うことでコードの実行速度を改善することができると学べたかと思います。Deopt Explorer というツールの解説を通して、こういった観点をみなさんに持ってもらえたなら幸いです。27パフォーマンス改善に栄光あれ!
終わり28