|
昨日のカウンタ: 今日のカウンタ: |
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がどうの言ってもこれって浮動小数点演算の話でしょ?」って話もあるでしょうが、それは次の話ということで。
お気楽にプログラムを書いてもサクサク動いてくれるといいな。使うリソース量とかあまり気にしないで「やって欲しいこと」を書くだけでコーディングまで終わるような時代になるといいな。でも今日もまだコードに「どうやってやるのか」を書かないといけないんだなぁ。
そんなことを日々思っているのですが、局所的な話としてこのシリーズでずっと例に出しているモノクロ化にしても「浮動小数点演算なんて遅いから固定小数点演算で」みたいな小手先の技がまだ有効だったりします。いや、もう頭のいい人が素晴しい最適化をしてくれてそんなの必要なくなってるんじゃない? と思いつつも裏切られる日々な訳だったりしますが、RenderScriptではどうでしょうか。
話を戻します。モノクロ化ですが、red, green, blueの色要素を0.288, 0.587, 0.114の比率で混ぜて、その数値を各色要素に戻すだけですので、例えば(red×288+green×587+blue×114)/1000とするのも一例です。が、「1000で割る」という計算の「1000」という数字に特は意味はありません。「256で割る」つまり「8ビット左シフトする」の方が一般には計算が早いのでそうしましょう。つまり今回は(red×76+green×150+blue×30) >> 8での計算を比較してみることにします。
NDKではこんな感じです。
static const int r_lum_i = 76; static const int g_lum_i = 150; static const int b_lum_i = 30; …(中略) for (int i = 0; i < length4; i += 4) { r = inp[i + 0]; g = inp[i + 1]; b = inp[i + 2]; m = (r * r_lum_i + g * g_lum_i + b * b_lum_i) >> 8; outp[i + 3] = 0xff; outp[i + 0] = outp[i + 1] = outp[i + 2] = m; }
RenderScriptではこんな感じです。FilterScriptもほとんど同じなので省略します。
static const uint r_lum = 76; static const uint g_lum = 150; static const uint b_lum = 30; void root(const uchar4 *v_in, uchar4 *v_out) { uchar l = (uchar)((v_in->x * r_lum + v_in->y * g_lum + v_in->z * b_lum) >> 8); v_out->x = v_out->y = v_out->z = l; v_out->w = 0xff; }
さて、速度はどうなるでしょう。小手先の技は悲しいことに通用してしまうのでしょうか? そして浮動小数点を使わない場合であってもRenderScriptはNDKより速いのでしょうか?
処理自身は昨日と同じです。1024×768サイズの画像を100回モノクロ化します。単位はms(ミリ秒)です。relaxの有無とFilterScriptについては昨日の日記を参照して下さい。
機種名 | Nexus 4 | Galaxy Nexus | Galaxy Note 8.0 | Stream X |
---|---|---|---|---|
Androidバージョン | 4.3 | 4.3 | 4.1.2 | 4.1.2 |
NDK | 3809 | 4954 | 1434 | 5501 |
RenderScript | 985 | 2788 | 772 | 2907 |
RenderScript (relax) | 3097 | 5341 | 766 | 2885 |
FilterScript | 3038 | 5406 | 801 | 2899 |
RenderScript (relax)とFilterScriptの差はあまりないので、やっぱりFilterScriptのことはそれほど真面目に考えなくてもいいのかなという点と、NDKとRenderScriptのどちらでも選べるシチュエーションではRenderScriptを選択した方が良さそうという点は昨日の浮動小数点演算の場合と同じでしょうか。やっぱりマルチコアへの展開をやってくれているんでしょうかね。
浮動小数点演算での計算速度の違いとしては、relax指定の付いてないRenderScriptの方がrelax付きよりも速いってところでしょうか。relax付きがホントにGPUで計算してくれているのなら、整数演算はCPUでやった方が効率的かもなぁ、そうかもなぁと一応納得はできるところです。が、やはり確証は持てませんが。
理屈で納得するのはいいとして、Android 4.3の整数演算では、relax無しのRenderScriptがrelax付きより2〜3倍速いというのは、アプリ実装時に悩ましい点であります。だって浮動小数点演算ではrelax付きの方が速い傾向にある訳ですから。
浮動小数点で書くべきところを小手先でやったらどうなるかという点については、昨日の表と比較すればわかりますが、やっぱり小手先の技が効いちゃってます。例えばNexus 4で見ると浮動小数点で最速だったFilterScriptよりも整数演算で最遅のNDKの方がまだ速いです。Galaxy Note 8.0で見ても浮動小数点での最速であるNDKよりも整数演算で最遅のNDKの方がずっと速いとか。小手先の技もまだ有効みたいなのは複雑な気分です。
まぁインラインアセンブラでNEONとかがりがり書きつつも、Open GL ES 3.0でGPGPU的にがりがり書けばもっと速いんでしょうけど。