- OpenGL ESの頂点シェーダーで毎度スキニングしない場合の処理について興味があった。
- CPU処理させたらクッソ重いハズ。
- RenderScript初体験としては良い課題なんじゃないかな?
- プログラム自体は基本的にフルスクラッチ
- C++11で記述
- IKやフォーマット解析等の部分については、書籍やいくつかのライブラリやウェブページを参考にした
- zahir labs
- MikuMikuSoft開発室
- MikuMikuPenguin
- 針金のブログ
- PさんのためのPMDエディタの本 MikuMikuDanceモデルセットアップ入門(翔泳社)
チェックした端末
- Nexus5
- Galaxy Nexus
- Nexus9
- SHIELD TABLET
評価対象オブジェクト
- Tda式ミク
- 25164頂点
- 1体表示
処理
- ソフトウェアスキニング〜CPUアクセス可能なメモリへ書き戻し完了までの時間を計測する。
- 最初の60フレームを切り捨て、その後300フレームの平均値を算出する
- 上記の方法で計測後、最も速い記録を採用する
- 上記の後、glBufferSubDataによって頂点のアップロードを行う
- BDEF1、BDEF2、BDEF4の設定を反映させた頂点スキニングを行う。
- コンパイルにclangを用いる
- C++で記述する
- 必要なメモリは可能な限りalloc済みにする
頂点ブレンディングのみ行う
CPU(単純実装)
- 頂点をfor文で全て処理
- 頂点ごとにスキニング処理をif文で分岐
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | 28.3ms | 1.9ms | 案外早い |
Galaxy Nexus | 65.0ms | 4.4ms | そこそこ速い |
CPU(hard-float有効化)
- hard-floatを有効化して、なるべくハードウェアを利用するように修正
Android.mk
LOCAL_ARM_MODE := arm
LOCAL_ARM_NEON := true
LOCAL_CPPFLAGS += -mfpu=neon
Application.mk
APP_ABI := armeabi-v7a-hard
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | 25.3ms | 1.5ms | かなり速くなる |
Galaxy Nexus | 51.0ms | 4.1ms | 速度に大差なし |
CPU(vec3 -> vec4に変更)
- 頂点構造体の位置情報にvec3を利用していた
- そのため、vec4に変更して一時オブジェクトを生成しないようにする
- メモリ消費量が微増(4byte/vertex)
- 速度差は誤差レベルでしか発生しなかった
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | 20.2ms | 1.4ms | 誤差レベル |
Galaxy Nexus | 53.0ms | 4.5ms | 誤差レベル |
CPU(glmのインライン化)
- GLM_FORCE_INLNIEで強制的にinlineにする
- 性能が上下したけど、誤差レベルな気がする
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 1.2ms | 多少早くなった? |
Galaxy Nexus | - ms | 4.8 ms | 多少遅くなった? |
CPU(全て1頂点として計算し、分岐を削除)
- ブレンド数の低下はかなり効果がある
- if文の削除はあまり効果がなかった(恐らく最適化のため)
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 1.0ms | 速度向上 |
Galaxy Nexus | - ms | 4.5 ms | 速度向上 |
CPU(事前に頂点タイプごとにグルーピングしてif文を回避)
- if文がコンパイラ最適化によってボトルネックにならないため、ほとんど速度差が生まれない
- for文内で分岐を行ったほうが、最適化によって良い結果を生むらしい
- 2.5万回もif文展開してもさしてボトルネックにならないのはスゴイな
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 1.7ms | 多少遅くなる |
Galaxy Nexus | - ms | 5.7 ms | 多少遅くなる |
CPU(複数Threadで分割処理)
- 適当な数のThreadに処理を分散する
- Threadの同期処理のほうがコストが高いため(?)か、処理が遅くなる
- 概ね、6Thread前後が良い結果となった
- 遅いことに変わりは無し
- 更に大量のデータを処理したら変わるかも?
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 3.5 ms | 遅い |
SHIELD TABLET | - ms | 3.8 ms | 遅い |
法線計算を加味する
1Threadでif文付きで計算させる
- MEMO:不要なThreadがあると処理に影響するので止めておく
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 2.8ms(r10d) → 1.9 ms(r10e) → 1.3ms(コード手動最適化) | NDKのバージョンアップで高速化 |
Nexus9 | - ms | 2.2 ms(r10d) → 1.7ms(r10e) → 0.9ms(コード手動最適化) | NDKのバージョンアップで高速化 |
SHIDLE TABLET | - ms | 2.9ms(r10d) | 多分検証すれば速くなってるハズ |
Zenfone2(ML550) | - | 2.1ms | コードをイジった後 |
複数Threadに分割する
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 5.1 ms(r10d) → 3.3 ms(r10e) | 速くはなったが、結構時間がかかる |
Nexus9 | - ms | 2.4 ms(r10d) | 速い |
RenderScript版
注意点
- 16byte単位にデータ境界が設定されるため、構造体の定義に注意が必要
- AllocationからGL用バッファへの書き戻しコストが非常に高くてあまり現実的じゃない
- 構造体をoutさせるとJava → NDKへのデータ転送が遅いため、I8配列として渡してRS内部で構造体にキャストする方法を選択した
- 書き戻し時間は減ったが、恐らく並列化の恩恵が少なくなるので実行時間は増える
- NDKから直接データをIOできる機能が欲しい(r10e時点で無し)
結果
端末 | デバッグビルド | 最適化ビルド | 備考 |
---|---|---|---|
Nexus5 | - ms | 4.0 ms | 遅い |
Nexus9 | - ms | 4.4 ms | 遅い |
まとめ
- 少量オブジェクトを変形させる場合は、CPU/1Threadでやったほうが軽くなりそう
- Thread制御のオーバーヘッドのほうがでかくなる
- アルゴリズムを見直すことによる手動のコード最適化も十分に効果的
- コンパイラばかりに頼らないほうが良い場合もある
- Makefileのオプションによっては更に高速化が行えると思われる
- コンパイラの最適化レベルを上げれば多分もうちょい早くなる?
- 未検証
- RenderScriptを頂点シェーダー代わりに使用するのは難しい
- RenderScriptからのメモリの書き戻し時間優先 or 処理速度優先のどちらかを取るしか無い
- RenderScriptへの書き込みは、多少の工夫で高速に(uncheckedなメソッドで)可能
- Android NDK r10d→r10eで処理速度が向上した
- 特定分野の処理だけかもしれないが、最適化されやすくなったのだろうか