|
昨日のカウンタ: 今日のカウンタ: |
AndroidのアプリをJavaで作っていて速度面で限界を感じた場合の解決方法の一つがNDKです。これはJavaの代わりにCやC++で重い処理を書いてCPUが直接実行できるバイナリを生成し、それをJNI経由でアプリから呼ぶという仕組みでした。
一方のRenderScriptでもNDKと同じようにC言語でコードを書くのですが、生成されるのはLLVMのBitCode*1という中間コードです。このBitCodeをCPUあるいはGPUやDSPで実行します。
「中間コードだったらJavaと同じじゃない。高速化無理でしょ。」と思うかも知れませんが、動かしてみると実際に高速化されています。(この話は後で書きます。)
ぶっちゃけRenderScriptの高速化の鍵は並列化です。通常であれば依存関係のない大量の処理はループなどで記述しますが、その依存関係のないループの中身だけを関数として記述するイメージです。つまり、依存関係のない処理の断片を大量に行うというシチュエーション限定で有効な技法と考えれば良いでしょう。 例えばAndroid SDKにはHelloComputeというサンプルプロジェクトが含まれています。ここにあるmono.rsの鍵となる部分は以下のようになっています。
const static float3 gMonoMult = {0.299f, 0.587f, 0.114f}; void root(const uchar4 *v_in, uchar4 *v_out) { float4 f4 = rsUnpackColor8888(*v_in); float3 mono = dot(f4.rgb, gMonoMult); *v_out = rsPackColorTo8888(mono); }
要は、一つのピクセルを入力(v_in)として、一つのピクセルを出力(v_out)とする関数で、輝度monoをred×0.299+green×0.587+blue×0.114と計算し、その値を出力のred, green, blue要素の返すというピクセルのモノクロ化関数です。
この関数を画像全体に適用させることで、最大でピクセル数個の並列化が可能となる訳です。このようにRenderScriptは、利用できるシチュエーションは限定されるのでしょうが、うまく使えば有効な高速化手法です。
あ、本当に高速化されているかどうかの話を書き忘れてましたが、それはこの次で。
*1 ビルドするとbcファイルが実際に作成されます。
RenderScriptは仕組みとしてはAndroid 2.2ぐらいから入っていたそうなのですが、APIとしてはAndroid 4.0からとなっています。
Javaのコードではないのでサポートライブラリみたいなのは提供されないんだろうなと思っていたらRenderScript in the Android Support Libraryなるものが10日ほど前に公開されました。
実際にこのサポートライブラリを使ってRenderScriptを書いてみると、arm/mips/x86の各プラットフォーム毎にオブジェクトファイルと共有ライブラリファイルもEclipse経由でプロジェクト内に作成されます。例えば先の記事のようにmono.rsを書くとmono.oやmono.soも作成されます。
先のブログ記事によれば、このサポートライブラリを使うと、Android 4.2までは、RenderScriptは事前にコンパイルされ、CPUでのみ実行される形式になるそうです。mono.soなどが「事前にコンパイルされたCPUでのみ実行される形式」なのでしょう。ちなみにサポートライブラリを使わなくてもAndroid 4.0以降ではRenderScriptを使えるのですが、その場合は、事前にコンパイルされることなく中間形式(BitCode, mono.bc)のままアプリにパッケージされ、アプリの実行時に解釈されるようです。
一方でAndroid 4.3ではmono.soは利用されず、mono.bcが利用され、CPU/GPU/DSPのいずれかで実行されることになるとのことです。
Android 4.3でBitCodeの解釈が賢くなったので、Android 4.2までのあまり賢くないBitCodeパーザ(インタプリタ? ランタイムコンパイラ?)を使うよりはSDKの段階で賢くコンパイルしちゃう代わりに、GPUやDSPでの実行は諦めることになるということなんでしょうね。
という訳でお待ちかねNDKとRenderScriptの速度比較例です。飛び入りでFilterScriptにも参加してもらいましょう。FilterScriptはAndroid 4.2でRenderScriptよりもGPUやDSPで実行されやすいものといて紹介されたのですが、Android 4.3のドキュメントからは消滅してしまいました。何があったのでしょうね。
画像のモノクロ化処理をLoopCount(=100)回実行します。 NDK側のコードを書くとこんな感じです。r_lum, g_lum, b_lumには先程と同じように0.288, 0.587, 0.114が各々代入されています。
JNIEXPORT void JNICALL Java_jp_sakira_hellorenderscript_HelloRenderScript_mono_1float_1calc (JNIEnv *env, jobject thiz, jobject outb, jobject inb) { void *inb_, *outb_; unsigned char *inp, *outp; AndroidBitmapInfo info; AndroidBitmap_getInfo(env, inb, &info); int length4 = info.width * info.height * 4; AndroidBitmap_lockPixels(env, outb, &outb_); AndroidBitmap_lockPixels(env, inb, &inb_); inp = (unsigned char*)inb_; outp = (unsigned char*)outb_; int m; float r, g, b; for (int j = 0; j < LoopCount) for (int i = 0; i < length4; i += 4) { // 実際のモノクロ処理はここから r = inp[i + 0]; g = inp[i + 1]; b = inp[i + 2]; m = (int)(r * r_lum_f + g * g_lum_f + b * b_lum_f); outp[i + 3] = 0xff; outp[i + 0] = outp[i + 1] = outp[i + 2] = m; } // 実際のモノクロ処理はここまで AndroidBitmap_unlockPixels(env, outb); AndroidBitmap_unlockPixels(env, inb); }
一方のRenderScriptではこんな感じです。先の記事と同じです。NDKの方はややこしく見えますが、Bitmapのロック処理などが入っているせいで、RenderScriptの方はJavaコードの方でやっちゃってる部分なので実質的には複雑さは同じぐらいです。
void root(const uchar4 *v_in, uchar4 *v_out) { float4 f4 = rsUnpackColor8888(*v_in); float3 mono = dot(f4.rgb, gMonoMult); *v_out = rsPackColorTo8888(mono); }
FilterScriptではポインタが使えないなどの制限により以下のように記述されます。どういう形式で、つまり、関数の型などはどうすれば良いかなどについてはstackoverflowの記事を参照しました。こんなのよく調べましたね。というか、こんな大事な事をドキュメント化せずに新機能として紹介するGoogleさんはどうなんでしょう。
uchar4 __attribute__((kernel)) root(uchar4 in, uint32_t x, uint32_t y) { float4 f4 = rsUnpackColor8888(in); float3 mono = dot(f4.rgb, gMonoMult); return rsPackColorTo8888(mono); }
ちなみにRenderScriptやFilterScript側には「100回回してBitmap画像に反映させる」処理が入っていませんが、Java側でこんな感じでいけます。
private void convertByRSFloat() { for (int i = 0; i < LoopCount; i++) m_script_float.forEach_root(mInAllocation, mOutAllocation); mOutAllocation.copyTo(mBitmapOut); }
という訳で、ソースに書いてるアルゴリズムとしては同一であるこれらの計算方法の速度を比較してみましょう。先に書いたようにAndroid 4.2までとAndroid 4.3ではビルド方法や実行方法の違いで違いが出そうです。また、Android 4.3ではGPUで実行される場合もあるのでハードウェア構成による違いも出そうです。ここでは手軽に試せる手持ちの機器からNexus 4 (Android 4.3), Galaxy Nexus (Android 4.3), Stream X (Android 4.1)を選んでみました。1024×768サイズの画像を100回モノクロ化する時間で、単位はms(ミリ秒)です。
機種名 | Nexus 4 | Galaxy Nexus | Galaxy Note 8.0 | Stream X |
---|---|---|---|---|
Androidバージョン | 4.3 | 4.3 | 4.1.2 | 4.1.2 |
NDK | 7724 | 10067 | 3099 | 8538 |
RenderScript | 6327 | 10279 | 3319 | 8434 |
RenderScript (relax) | 3954 | 11760 | 3285 | 8265 |
FilterScript | 4001 | 11751 | 3305 | 8329 |
さていきなり出てきた「RenderScript (relax)」ですが、これはGPUでの動作を促進するために浮動小数点演算の精度を落とすというpragmaによる指示を付けたものです。詳しくはRenderScriptのドキュメントを読んで下さい。(あ、投げた) 眺めるととりあえずRenderScript (relax)とFilterScriptの差は誤差範囲なのかなという印象です。実際にGPUで動いているのかどうかわかりません、というかそれを判断する方法を思いつきませんでしたが、Android 4.2までではrelaxを付けたらほぼ確実に劇遅になってしまっていたので、進展はしているのでしょう。GPUで動いてるかどうかは知らない*1ですが。(くどい)
あと、NDKと素のRenderScriptの比較では、Galaxy Note 8.0での例外はありますが、NDKより速い場合が多いという結果になりました。サンプル少ないですが。ここもAndroid 4.2に比べると大きな進歩と言えます。*2
あとはRenderScriptの素のものとrelax付きの差ですが、Nexus 4で顕著ですが比較的新しい機種でrelaxの方が速くなるんじゃないのかなと思うちゃう傾向が出ているように見えます。というかrelax使って遅くなったとしてもとても遅くなる訳ではない一方で、速くなる時にはかなり速くなるようなので、これからは積極的に使ってもいいんじゃないかなという印象です。サンプル少ないですけど。
まぁFilterScriptがドキュメントから消失した理由ですが、この結果を見る限りはrelax付きのRenderScriptで同じ効果が出るみたいだから削ったのかなと思ったりもします。まぁ、こんな単純なサンプルでそこまで言うなって言われそうですが。
あと、NDKでNEONとかのSIMD使えよって話もあるんでしょうが、今回はとりあえずここまでとさせて下さい。こんだけでも結構疲れました。(苦笑)
「relaxがどうのFilterScriptがどうの言ってもこれって浮動小数点演算の話でしょ?」って話もあるでしょうが、それは次の話ということで。