2008年02月05日(火)
ActionScript最適化 多分その1 ガベージコレクタを攻略する
前回のサンプルの描画サイズを変更して16x9(640x360ピクセル)にしてみた。
描画面積が減ってその分パフォーマンスが稼げると同時に、次世代機っぽくなって良い。AS3のパフォーマンスが予想よりやや厳しいため描画サイズを減らしたが、もしかすると320x180ピクセルくらいでも適当かもしれない。往年のメガデモ?
さて、AS3環境でのパフォーマンスについて若干調べてみた。
●プロファイリング
むかーしのCEDECでも講演したことがあったが、パフォーマンス最適化の基本はプロファイリング。よいプロファイラ環境があればチューニングはほぼ終わったといっても過言ではない。過言か。
ActionScript環境ではFLEX用のプロファイラが提供されている。FLEX環境が必要とのことで、来るFLEX移行時に試すことにする。
http://www.adobe.com/devnet/flex/articles/profiler.html
また↓はprototypeにプロファイリング用の仕掛けを入れるタイプのプロファイラらしい。
http://www.nochump.com/asprof/
今回の計測に当たっては
flash.utils.GetTimer();
を使った。
コード規模が少なくて助かったが、上記プロファイラの存在を知っていればもっと楽だったかもしれない。
GetTimer()は解像度が1msecと荒いため詳細のプロファイリングには向いてないが残念ながらAS3環境ではこれ以上の解像度のタイマがなさそうだ。
●最初の計測結果
1フレームあたりの総処理時間と更新/描画処理の処理時間を計測したものが下のグラフ(ここでGoogleChartAPIを小一時間ほど触ってみて挫折。値を正規化しないといけないのがめんどい。サービスを作れという思し召しだろうか)
グラフ上で、青線が総実行時間、赤線が更新/描画処理の負荷(単位msec)。
殆どの実行時間が更新、描画処理に喰われているのが分かる。
また、1フレームあたり60msec~120msecと処理時間は安定せず。且つ邪悪なスパイク(処理負荷の増大)のようなものが見て取れる。
現状で秒間10~12フレームというところだろう。
●更新処理、描画処理の内訳
次に、更新/描画処理内での負荷傾向を調べるために、更新処理だけを計測したもの(単位msec)。
演算を全て倍精度の'Number'で行なっており(ECMAScript仕様では80bit精度、ActionScriptでは64bit精度とされている)結構処理を喰っているのでは、と想定したが以外と低負荷で処理されている。
一見して分かるとおり、定期的に12msec前後のスパイクが見られる。
●スパイクは許されざるものbyクリント・イーストウッド
解説:
ゲームプログラムではこうした一時的に処理負荷が高まる現象は風水的に良くないとされて忌み嫌われる。
ゲーム画面はテレビモニタのリフレッシュレートに併せて毎16.6msec(または33msec等16.6mecの整数倍)毎の定期的な感覚で画面更新を行なっている。その際に1フレームでも画面更新が遅れるとユーザにも分かる画面の引きつりとなってゲームへの没入が阻害されてしまうのだ。このため処理負荷が一時的に増大するスパイクはドライバやアプリケーションの実装では徹底的に避けるのだ。
●ガベージコレクタ
マルチプロセス/プリエンプティブなWindows環境ではスパイクを含めて何が起きても不思議ではないのだが、とりあえずその原因を追ってみることにする。
VM環境での実行時間のスパイクについてまず疑わしいのはAVM2が行なっているガベージコレクション処理だ。
これを確認するため、確保済みメモリサイズと先ほどの更新処理負荷を併せて計測してみる。
#なお、その他にはスレッドのタイムスライスによるタイムアウト、ドライバのへぼい処理、変な同期処理、IO待ち、などが考えられる
青線が使用メモリサイズ(左側単位byte)、赤線が更新処理負荷(単位msec)。
スパイクとメモリサイズの変動が一致しているため、ガベージコレクションによる負荷がスパイクを招いていると言ってよいだろう。
一般的にガベージコレクション中は全ての処理を停止させるため、納得のいく現象だ。
図よりAVM2のガベージコレクタKickOffThresholdが1MB前後なのも見て取れる。
AVM2のガベージコレクタのアルゴリズムは↓の資料が参考になる
http://www.onflex.org/ACDS/AS3TuningInsideAVM2JIT.pdf
資料によるとAVM2では
・Deferred Reference Counting
・incremental conservative mark/sweep
の2手法を使っているようだ。
リファレンス・カウンタは効率的だが、オブジェクトの循環参照が発生したときに永久に開放されないゾンビオブジェクトが発生してしまう。
この弱点を補うために、全ノードの参照カウンタをトップノードから洗いなおすMark&Sweep方式を定期的に発動させていると思われる。
またスタック上のオブジェクトに対するリファレンスカウンタ操作を行なわない点をDefferredと呼んでいるようで、これにより20%の性能向上が観られるとは興味深い結果だ。
#なお.Netでは上記に加えて生き残ったオブジェクトを世代別に管理する世代別GCを採用して更に実行効率を高めている。
Mark&Sweepの欠点はGC発動中にリファレンスカウンタが飛んでしまうため他の処理の動作をとめる必要がある点だ。この点はリファレンスカウンタを別領域にDUPしてその時点で参照されていないオブジェクトだけを抽出する方式(且つオブジェクトの動的リロケーションを伴わない条件で)回避できるような気もする。
また、実際オブジェクトの循環参照がどれくらい発生するかは個人的には不思議な点。リファレンスカウンタを基本にリクエストに応じた場合だけMark&Sweepという実装ではどうだろうか、と考えてみる今日この頃だ。
●ガベージコレクタ攻略戦
状態更新処理中にガベージコレクタによるスパイクが発生しているため、これを回避することにする。
ガベージコレクタの発動を抑えるためには、基本的には下記の手法が有効だ。XNA(.Net)やJAVAのゲームプログラミングでもガベージコレクションを抑制として同様の手法が有効だ。
・ダイナミックなアロケーションを避ける
-関数内で一時オブジェクトをnewしている場合は、メンバ関数としてオブジェクトをstaticに確保。メモリ喰ってもガベージコレクタよりはマシだ。
-戻り値を確保している場合は関数シグネチャを変更して引数に戻り値の参照を持たせる
-万難排してnewを避ける
・BOXINGにも気をつける
-Boxingによる暗黙の型変換用オブジェクトの生成も排除する。Boxing用の静的オブジェクトを確保しておいてそれを使用すると良い
・どうしてもアロケーションされてしまうときは、気休めにdispose()/destroy()などしてみる
API等で確保されてしまうオブジェクトについては止むを得ず、神に祈る。disposeしたとしても廃棄リストへ追加されるだけでガベージコレクタ発動抑制の役には立たないことが殆どだろう。
●ガベージコレクタ・もう一点の謎
大抵上記のような手順でガベージコレクタの挙動は大分制御できたが、AVM2の場合もう一点不可解な挙動が見られた。
どうも特定の演算で一時オブジェクトが生成されているような気配がある。かなり基本的な演算であることと、ABC(アクションスクリプト・バイト・コード)ダンプを確認してもそれらしいコード生成は行なわれていない点もあり、トンデモ学説かもしれない。この点についてはも少し検証したい。
上記のような改修の結果
状態更新において不機嫌なスパイクは解消した。ところにより処理時間が2msecとスパイクのように見えるが、タイマ解像度が1msecしかないための誤差範囲と考えてよいだろう。また、メモリ使用量がなだらかに上昇しているためもう少しガベージコレクタ対応の余地があるが、とりあえずは許容範囲とする(してください)。
●最適化途中結果
状態更新ループ中でのガベージコレクション発動を抑えた結果
まだまだフレームレート安定には遠い道のりだが、変動は若干抑制された、ような気がする。
つづいて(続けば)描画周りなどを検証してみる予定だ。
描画面積が減ってその分パフォーマンスが稼げると同時に、次世代機っぽくなって良い。AS3のパフォーマンスが予想よりやや厳しいため描画サイズを減らしたが、もしかすると320x180ピクセルくらいでも適当かもしれない。往年のメガデモ?
さて、AS3環境でのパフォーマンスについて若干調べてみた。
●プロファイリング
むかーしのCEDECでも講演したことがあったが、パフォーマンス最適化の基本はプロファイリング。よいプロファイラ環境があればチューニングはほぼ終わったといっても過言ではない。過言か。
ActionScript環境ではFLEX用のプロファイラが提供されている。FLEX環境が必要とのことで、来るFLEX移行時に試すことにする。
http://www.adobe.com/devnet/flex/articles/profiler.html
また↓はprototypeにプロファイリング用の仕掛けを入れるタイプのプロファイラらしい。
http://www.nochump.com/asprof/
今回の計測に当たっては
flash.utils.GetTimer();
を使った。
コード規模が少なくて助かったが、上記プロファイラの存在を知っていればもっと楽だったかもしれない。
GetTimer()は解像度が1msecと荒いため詳細のプロファイリングには向いてないが残念ながらAS3環境ではこれ以上の解像度のタイマがなさそうだ。
●最初の計測結果
1フレームあたりの総処理時間と更新/描画処理の処理時間を計測したものが下のグラフ(ここでGoogleChartAPIを小一時間ほど触ってみて挫折。値を正規化しないといけないのがめんどい。サービスを作れという思し召しだろうか)
グラフ上で、青線が総実行時間、赤線が更新/描画処理の負荷(単位msec)。
殆どの実行時間が更新、描画処理に喰われているのが分かる。
また、1フレームあたり60msec~120msecと処理時間は安定せず。且つ邪悪なスパイク(処理負荷の増大)のようなものが見て取れる。
現状で秒間10~12フレームというところだろう。
●更新処理、描画処理の内訳
次に、更新/描画処理内での負荷傾向を調べるために、更新処理だけを計測したもの(単位msec)。
演算を全て倍精度の'Number'で行なっており(ECMAScript仕様では80bit精度、ActionScriptでは64bit精度とされている)結構処理を喰っているのでは、と想定したが以外と低負荷で処理されている。
一見して分かるとおり、定期的に12msec前後のスパイクが見られる。
●スパイクは許されざるものbyクリント・イーストウッド
解説:
ゲームプログラムではこうした一時的に処理負荷が高まる現象は風水的に良くないとされて忌み嫌われる。
ゲーム画面はテレビモニタのリフレッシュレートに併せて毎16.6msec(または33msec等16.6mecの整数倍)毎の定期的な感覚で画面更新を行なっている。その際に1フレームでも画面更新が遅れるとユーザにも分かる画面の引きつりとなってゲームへの没入が阻害されてしまうのだ。このため処理負荷が一時的に増大するスパイクはドライバやアプリケーションの実装では徹底的に避けるのだ。
●ガベージコレクタ
マルチプロセス/プリエンプティブなWindows環境ではスパイクを含めて何が起きても不思議ではないのだが、とりあえずその原因を追ってみることにする。
VM環境での実行時間のスパイクについてまず疑わしいのはAVM2が行なっているガベージコレクション処理だ。
これを確認するため、確保済みメモリサイズと先ほどの更新処理負荷を併せて計測してみる。
#なお、その他にはスレッドのタイムスライスによるタイムアウト、ドライバのへぼい処理、変な同期処理、IO待ち、などが考えられる
青線が使用メモリサイズ(左側単位byte)、赤線が更新処理負荷(単位msec)。
スパイクとメモリサイズの変動が一致しているため、ガベージコレクションによる負荷がスパイクを招いていると言ってよいだろう。
一般的にガベージコレクション中は全ての処理を停止させるため、納得のいく現象だ。
図よりAVM2のガベージコレクタKickOffThresholdが1MB前後なのも見て取れる。
AVM2のガベージコレクタのアルゴリズムは↓の資料が参考になる
http://www.onflex.org/ACDS/AS3TuningInsideAVM2JIT.pdf
資料によるとAVM2では
・Deferred Reference Counting
・incremental conservative mark/sweep
の2手法を使っているようだ。
リファレンス・カウンタは効率的だが、オブジェクトの循環参照が発生したときに永久に開放されないゾンビオブジェクトが発生してしまう。
この弱点を補うために、全ノードの参照カウンタをトップノードから洗いなおすMark&Sweep方式を定期的に発動させていると思われる。
またスタック上のオブジェクトに対するリファレンスカウンタ操作を行なわない点をDefferredと呼んでいるようで、これにより20%の性能向上が観られるとは興味深い結果だ。
#なお.Netでは上記に加えて生き残ったオブジェクトを世代別に管理する世代別GCを採用して更に実行効率を高めている。
Mark&Sweepの欠点はGC発動中にリファレンスカウンタが飛んでしまうため他の処理の動作をとめる必要がある点だ。この点はリファレンスカウンタを別領域にDUPしてその時点で参照されていないオブジェクトだけを抽出する方式(且つオブジェクトの動的リロケーションを伴わない条件で)回避できるような気もする。
また、実際オブジェクトの循環参照がどれくらい発生するかは個人的には不思議な点。リファレンスカウンタを基本にリクエストに応じた場合だけMark&Sweepという実装ではどうだろうか、と考えてみる今日この頃だ。
●ガベージコレクタ攻略戦
状態更新処理中にガベージコレクタによるスパイクが発生しているため、これを回避することにする。
ガベージコレクタの発動を抑えるためには、基本的には下記の手法が有効だ。XNA(.Net)やJAVAのゲームプログラミングでもガベージコレクションを抑制として同様の手法が有効だ。
・ダイナミックなアロケーションを避ける
-関数内で一時オブジェクトをnewしている場合は、メンバ関数としてオブジェクトをstaticに確保。メモリ喰ってもガベージコレクタよりはマシだ。
-戻り値を確保している場合は関数シグネチャを変更して引数に戻り値の参照を持たせる
-万難排してnewを避ける
・BOXINGにも気をつける
-Boxingによる暗黙の型変換用オブジェクトの生成も排除する。Boxing用の静的オブジェクトを確保しておいてそれを使用すると良い
・どうしてもアロケーションされてしまうときは、気休めにdispose()/destroy()などしてみる
API等で確保されてしまうオブジェクトについては止むを得ず、神に祈る。disposeしたとしても廃棄リストへ追加されるだけでガベージコレクタ発動抑制の役には立たないことが殆どだろう。
●ガベージコレクタ・もう一点の謎
大抵上記のような手順でガベージコレクタの挙動は大分制御できたが、AVM2の場合もう一点不可解な挙動が見られた。
どうも特定の演算で一時オブジェクトが生成されているような気配がある。かなり基本的な演算であることと、ABC(アクションスクリプト・バイト・コード)ダンプを確認してもそれらしいコード生成は行なわれていない点もあり、トンデモ学説かもしれない。この点についてはも少し検証したい。
上記のような改修の結果
状態更新において不機嫌なスパイクは解消した。ところにより処理時間が2msecとスパイクのように見えるが、タイマ解像度が1msecしかないための誤差範囲と考えてよいだろう。また、メモリ使用量がなだらかに上昇しているためもう少しガベージコレクタ対応の余地があるが、とりあえずは許容範囲とする(してください)。
●最適化途中結果
状態更新ループ中でのガベージコレクション発動を抑えた結果
まだまだフレームレート安定には遠い道のりだが、変動は若干抑制された、ような気がする。
つづいて(続けば)描画周りなどを検証してみる予定だ。

リンク元(referer)
Google.co.jp ActionScript 負荷 検証
Google.co.jp actionscript ガベージコレクタ
Google.co.jp flex3 プロファイラ 単位
Google.co.jp ActionScript3 オブジェクト 廃棄
Google.co.jp ActionScript 最適化
Google.com ActionScript GC 循環参照
Google.co.jp flex ガベージコレクタ
Google.co.jp 循環コレクタ 世代別GC
Google.co.jp Flex ガベージコレクション
Google.co.jp ガベージコレクション as3
Google.co.jp actionscript ガベージコレクタ
Google.co.jp flex3 プロファイラ 単位
Google.co.jp ActionScript3 オブジェクト 廃棄
Google.co.jp ActionScript 最適化
Google.com ActionScript GC 循環参照
Google.co.jp flex ガベージコレクタ
Google.co.jp 循環コレクタ 世代別GC
Google.co.jp Flex ガベージコレクション
Google.co.jp ガベージコレクション as3







ためになります!