Archive for 5月, 2013

無効領域と遮蔽

Posted 2013.05.03 in 吉里吉里

めざせ60FPS!の記事で、Foooの無効領域処理と遮蔽処理による最適化を紹介したが
この二つの最適化は、実は吉里吉里でも行われている。

無効領域処理

無効領域処理は常に自動的に行われている。
吉里吉里のウィンドウをアクティブにして「Shift+F11キー」を押すと
更新矩形の発生の様子を確認できる。
複数の矩形で管理されているため、期待通り効率的に動作する。

無効領域は描画関数を使ったり、レイヤーの見た目が変化する操作をすると自動的に発生する。
Layer クラスの update 関数で明示的に無効領域を発生させることもできる。
プラグインで新たに描画関数を作る時は、
描画を行った範囲を update 関数を使って無効領域にする必要がある点に注意が必要だ。
そうしないと描画が画面に反映されない。

ちなみに update 関数には少し癖がある。
詳しくは onPaintが二度呼ばれる問題 の記事を参照のこと。

遮蔽処理

吉里吉里が遮蔽処理を行うことはあまり知られていないかもしれない。
遮蔽処理は無効領域処理と違って、常に行われるわけではない。
遮蔽処理が行われるのは、レイヤーが不透明であることが保証される場合だけである。
言い変えるとレイヤーが

type==ltOpaque && opacity==255

の時に遮蔽処理が行われるということだ。
(厳密に言うともうちょっと複雑だが)

遮蔽処理をいつの間にか使っているケース

描画処理を少しでも軽くしようと思案した時まず思いつくのが
背景レイヤーの type を不透明度を考慮するデフォルトのltAlphaではなく、
不透明度を無視する ltOpaque にすることだろう。
背景は常に不透明であり、不透明度が必要ないからだ。

背景レイヤーの face を書き込み先の不透明度を保証しない dfOpaque にすると、
さらに描画の効率化が期待できる。
もっとも face はデフォルトで dfAuto なので、type を ltOpaque にすると
いちいち face を設定しなくても dfOpaque が設定された状態と同じになる。

face が dfOpaque だと ltAdditive のような不透明度を考慮しない高速なレイヤ表示タイプを
子レイヤーに対して使うことができるというメリットもある。

そうこうしてレイヤーの type を ltOpaque にすると…そう
「type==ltOpaque かつ opacity==255」の条件を満たすことで
遮蔽処理が行われるようになる。
遮蔽処理のことを知らなくても自動的にさらに描画が効率化されるというわけだ!

とは言え…視覚的には何も変わらないので、
遮蔽処理が行われている事実はわかりにくいかもしれない。

まとめ

無効領域処理と遮蔽処理の効果は、Foooの動作サンプルを見てもわかる通り絶大である。
その存在を意識することで、より効率的な動作をさせることが可能となるだろう。
積極的に活用していきたいところだ。

が!
………遮蔽処理には大きな問題がいくつか潜んでいる。
そのお話はまた次回~


== null が激重(再検証)

Posted 2013.05.02 in 吉里吉里

前回の記事で、== nullが激重になる原因が例外が投げられていることにあると述べたが
ソースコードを見て推測しただけで、きっちりトレースして調べたわけではなかったので
本当にそうなのか詳しく調べてみた。

現象の確認

Borland C++ Builder 6 のデバッガでステップ実行してみたところ
やはり例外が投げられていた。
eTJSVariantErrorという種類の例外だ。

この例外はtTJSVariant::NormalCompareのtry-catch節ですぐに捕まえられて
何事もなかったかのようにひっそりと処理されてしまうため
コンソールやSystem.exceptionHandlerなどでは捕捉できない。

負荷の特定

例外が投げられているのは確かだが、
異なる型との == null が重い原因は本当に例外のせいなのだろうか?
負荷の原因を細かく特定するため、ソースコードをいじって速度を計測してみた。

(1) 改変なし 11554 ms
(2) AsReal で TJSThrowVariantConvertError(); を呼ばずに、
throw eTJSVariantError(L””); としたもの。
6448 ms
(3) AsReal で TJSThrowVariantConvertError(); を呼ばずに、
throw 0; としたもの。
6357 ms
(4) AsReal で TJSThrowVariantConvertError(); を呼ばずに、
return 0; としたもの。
74 ms
(5) AsReal を呼ぶ前に左辺と右辺のどちらかが tvtObject なら
return false; としたもの。
47 ms

結果から、次のことがわかる。

例外処理が負荷の半分を占めている

そしてもう半分を TJSThrowVariantConvertError 内の処理が占めている。
TJSThrowVariantConvertError 内で行われている処理は
エラーメッセージを作成する文字列処理である。

修正案

実験結果から例外の負荷が非常に大きいとわかったので、
これを回避する方法について考えて見よう。

例外を投げているのはAsRealなので、
AsRealが例外を投げないようにする案がすぐ思いつくが、それはかなりデンジャラスである。
AsRealは他の場所でも使われているし、何よりAsRealが例外を投げるのはおそらく仕様上の動作であり、
その改変による影響範囲は計り知れない。

ここは実験(5)でやっていたように、
AsRealを呼ぶ前にtvtObjectを別途処理するようにするのが最もシンプルで安全な解決策だろう。
そもそもAsRealが呼ばれる時点で、型はInt、Real、Object、Octetの組み合わせしかないので
エラー検出をAsRealにわざわざ頼る必要は、もはやあまりないように思う。

tvtObjectやtvtOctetを判定ではじいて別途処理した場合、
判定回数が増加するというデメリットがある。
しかしそのデメリットは判定の優先順位の見直しである程度軽減できるだろう。
ObjectやOctetと他の型の値を比較するケースより、
IntとRealを比較するケースの方が圧倒的に多いであろうからだ。

さらなる最適化

またこれはあくまでアイデアにすぎないが、tvtObjectのような型の種類を表す定数を
ビットフラグ的な値にすれば、判定をより効率化できるかもしれない。


if(vt==tvtString || val2.vt==tvtString){ }
if(vt==tvtVoid || val2.vt==tvtVoid){ }
if((vt==tvtInt || vt==tvtReal) && (val2.vt==tvtInt || val2.vt==tvtReal)){ }
if(vt==tvtObject || val2.vt==tvtOject){ }
if(vt==tvtOctet || val2.vt==tvtOctet){ }

のような判定は


tjs_unit32 IntRealOR = tvtInt | tvtReal;
tjs_uint32 vtOR = vt | val2.vt; 
if(vtOR & tvtString){ }
if(vtOR & tvtVoid){ }
if(vtOR == IntRealOR){ } // vt != val2.vt なので == で済む
if(vtOR & tvtObject){ }
if(vtOR & tvtOctet){ }

に簡略できる。

まとめ

などなどいろいろ書いてみたけれど…
=== を使えばおおよそ問題を回避できるので、とりあえずそれで!