2010年12月26日日曜日

Graphicsのクリア忘れに注意

業務でやってたシステムで発生した問題。Flexで独自コンポーネントを作ったときに、updateDisplayListでGraphicsオブジェクト捕まえて、beginホゲホゲなどdrawホゲホゲなどやるのはよくあることだと思う。

その時に気を付けないといけないのが、Graphics.clear()の呼び忘れ。特に透明なしのベタ塗りだと、見た目は全く一緒なので、全く気づかないと思う。

では何が起こるかというと、描画コストがupdateDisplayList毎に増えていく。特にBitmapData.drawをするとよくわかる。簡単に言うと、ベタ塗り100回clearせずに行えば、BitmapData.drawを呼び出したときに、BitmapDataに100書き込まれるわけだ。

Flexでアニメーションを行おうとすると、領域が大きく、また子要素も多いと、フレームレートが低くなりがちである。その際に役に立つのが対象のオブジェクトを一旦BitmapDataに書きこんで、Bitmapをアニメーションさせるのである。フェードでも役に立つ。

開発しているシステムでは、これが徐々に遅くなり、イベントからアニメーション開始まで200msくらいフリーズした状態となっていた。しかも、連続運用4時間くらいから傾向が出始め、プロファイラで見てもメモリは増えてない。見つけるまで一週間かかった。みんなも気をつけましょう。。。

2010年12月24日金曜日

Tweenの高速化についてまとめ。その3

また内容がコアな感じになるが、次はJITの話。

アニメーションライブラリにエラーハンドリングを入れる際に気づいたのが、JITを殺すようなコードを書いてはいけないということ。そもそもFlashにおけるJITとは何か。それはバイトコードをインタプリタで動かしている状態で、よく使われる箇所をネイティブコードに動的に置き換えてしまう技術である。正式な名前はJust In Time Compilerである。

ではJITを殺すようなコードとは何か。まずはサンプルを見たほうが早い。

try/catchによってJITが効かなくなる件の検証 - wonderfl build flash online

大きく違うのが、関数の中にtry/catchが含まれているかの部分である。実行されるかは関係ない。他に検証してみたが、with句と関数定義が含まれているケースでも、JITが効いていないのが確認できた。

逆に言うとtry/catch/with/functionが含まれていなければJITの対象となるようである。上記コードでもtry/catchの部分だけ関数化した検証も含んでいるが、JITが効いているようである。

確認はしていないが、コンストラクタもJITは効かないようである。「関数呼び出しの時間」 < 「JIT無効の処理時間」 - 「JIT有効の処理時間」を満たすようなコード、簡単に言うとコンストラクタが忙しいコードの場合は、コンストラクタ内に書いているコードを初期化関数の呼び出しに変えて、初期化関数内で忙しい処理を行えば良い。ただし、関数呼び出しのオーバーヘッドを考慮する必要がある。

とは言え、最適化の話であるので、呼ばれる頻度の低いケースに置いては可読性や可用性を重視した方がよい。最適化原理主義者になってはいけない。

2010年12月22日水曜日

アニメーションライブラリのアルファ版を公開

アニメーションライブラリのアルファ版を公開した。URLはこちら。

コアについては、高速化の為に恐ろしく気持ち悪いコードになっていて、publicやらinternalのvarが沢山ある。OOP狂信者からすると死刑レベルである。これは高速化の為にやむを得なくやっているわけであって、決してOOPよくわかりませ~ん。というわけではない。

カプセル化が美しいコードとよくいうが、カプセル化のために関数経由で変数を参照していたら日が暮れてしまうのだ。AVM2はそんなに速くはない。

そこで、ファクトリとなるクラスを導入して、コア実装部分はinternalクラスとして外部からは不可視とし、インターフェイスを経由して外部から操作できるようにしている。コア実装部分ではアニメーションマネージャとアニメーションオブジェクト同士が、密にpublicなvarを直接参照しあっているのだが、外部からはインターフェイスを経由してget/setでしか触れないようにしている。中が汚くても外には見せてはいけない。

クラス数はそこそこに多いのだが、外部から見えるのはインターフェイスばっかりで、コンクリートクラスは少ないと思う。拡張性に難があるのだが、プラグイン的な機構は用意しているし、増やしていこうと思う。直接コアを拡張したいなんてニーズは少ないと思うし。

当面としては、基本機能のテストをしながら、Tweenerと同じパッケージ名、同じクラス名のAPIを提供したいと思っている。SWCを差し替えるだけで高速化できるというだけで、かなりなニーズがあると思う。

2010年12月20日月曜日

Tweenの高速化についてまとめ。その2

前回はリンクリストの手法について書いたけど、次は使用メモリについて。

アニメーションを高速化させる上で、高速化の邪魔となるのがGCだったりする。GCによって、非到達オブジェクトのマーキング、マークされたオブジェクトの破棄、領域のデフラグメントなどが行われる。60fpsのアニメーションの途中でGCが発生すると、60fpsを一瞬切ることもあり、体感としても一瞬詰まる感じもする。

次に試してみて欲しいのが以下のコードである。

プロパティの設定方法によって、メモリの利用量が変わる件のテスト - wonderfl build flash online


どう見ても同じオブジェクトの同じプロパティにNumberの値を代入しているだけでも、プロパティが静的に解決できているケース以外、代入するたびにメモリーが増えているのだ。これは不可解である。ちなみに代入する値がStringだと発生しない。また、プロパティが静的に解決できるケースにおいても全く増えない訳ではないが、微々たるものである。

仮に10000パーティクルを動かすとすると、毎フレームごとに数十から数百KBytesずつ消費され、1秒間だけでもあっという間に数十MBytes消費してしまう。実際にはリークしているわけではないので、GCされるが、当然重くなる。

今作成中のアニメーションのライブラリについては、動的な型と名前から、静的なプロパティへの代入処理にマッピングしなおすことによって、代入時のメモリ増を押さえてある。

2010年12月17日金曜日

Tweenの高速化についてまとめ。その1

最近作っている、そこそこに高速なTweenのライブラリについて、主に高速化の手法についていくつかまとめ。

まずは定番ではあるが、配列を使わずにリンクリストを使う。やはりFP10.1ではVectorよりも速い。ただし、要素の削除に伴う付け替えのコストが高い。削除による要素の付け替えの場合を考えてみると、以下の処理が必要となる。
  • 削除対象の前がnullなら次の要素がヘッドになる
  • 削除対象の次がnullなら前の要素がテイルになる
  • 削除対象の前後両方がnullならヘッドもテイルもnullになる
  • 削除対象の前後両方がnullでなければ、前後を繋ぎ合わせる
IF文は高速化の邪魔であるので、まずは不要な判定を減らしたいところである。そこで、ヘッドもテイルもダミーの要素として、事前に用意するのである。すると削除対象の前後が絶対にnullにならないので、IF文は不要になるのである。

リンクリストはwhile文などで回すことが多いと思うが、ループ毎にループを抜ける判定が必要になる。処理の実行回数にたいしてループの判定を減らすことによって高速化ができる。その手法として、ループの展開(アンロール)と呼ばれる手法がある。

例えばループを1ループあたり8回処理するようにすると、ループ回数は8で割った数になる。では処理回数が8で割り切れない場合はどうするか。普通に考えると、ループ内で都度、「まだ処理は必要か」と判定を行うことになる。それではアンロールの効果はないし、本末転倒である。

そこで、リンクリストのテイルに着目してみよう。テイルの次の要素は最後なのでnullである。しかし、テイルの次の要素がテイル自信の場合は、次、次と辿っても、nullが現れることはないのだ。つまり、nullであるかの判定が不要なので、「まだ処理は必要か」という判断がいらないのである。

次回はメモリ関連について書いてみる予定。

2010年12月12日日曜日

Math.powでの高速化について

Math.pow(n, 2)と書くくらいなら、n*nと書いた方が速いのは周知のとおり。では、Math.pow(n, m)の場合はどうか。mはuintとするが不定である。

累乗の計算のパフォーマンスの比較 - wonderfl build flash online


結果からすると、Math.powの方が圧倒的に遅い。しかしデバッグ実行では、ifよりswitchの方が遅く、また5乗あたりからMath.powの方がif/switchよりも速くなる。三項演算子は速いまま変わらない。

JITの特性の問題かもしれません。