Android 3.x RenderScript (7) RenderScript Compute の速度

RenderScript の用途は大きく分けて二通りあります。
Compute と Graphics です。

Compute (rs_cl) は rsForEach() 命令を使い、CUDA/OpenCL のような
並列処理を意識した呼び出し方となっています。
おそらくマルチコア CPU による並列化や GPU が将来的にサポートされる
のではないかと考えられます。

Graphics (rs_graphics) は少々特殊で、描画ループを Native 実行する
C言語で記述できることが特徴です。今までにないレイヤーですが、
NDK の部分的な置き換えを狙っているようにみえます。
またシームレスに rs_cl を呼び出せるのも大きなメリットです。

RenderScript は他にも vector や matrix 演算がサポートされており、
NEON 等の SIMD 命令に展開されるのではないかと考えられます。
ただし現状だと Android 3.x (Honeycomb) デバイスの大半が
NEON 命令が使えない NVIDIA Tegra 250 (Tegra2) なので、
その効果を確認することができませんでした。

●速度テスト

RenderScript は本当に速いのか、実際に速度を調べてみました。
Matrix4x4 * float4 を 100万頂点分演算します。

Android 3.1 API Level 11
LG Optimus Pad L-06C
Tegra250 1GHz (Cortex-A9 dual) NEON 無し VFPv3-D16

(1) RenderScript rsMatrixMultiply      160msec
(2) RenderScript 乗算展開              248msec
(3) Java 乗算展開                      571msec
(4) Java android.opengl.Matrix 呼出   2537msec
(5) NDK C++ 関数呼び出し               167msec
(6) NDK C++ 乗算展開                   168msec
(7) NDK inline asm fmacs               117msec
(8) NDK inline asm fmacs + pthread      72msec

(実行時間。値が小さいほうが高速)

データの転送時間は含まれていません。演算とループのみです。
ただし (8) に関してはスレッドの起動時間、終了同期(join) の時間が
含まれています。

●RenderScript

(1) は確かに速いですが、普通に NDK と C++ だけで書いたコード
(5)/(6) とほとんど差がありませんでした。
NEON があればもっと大きく差がついたのかもしれません。

同時に dual core CPU であっても、RenderScript は特に並列実行
していないことがわかります。

(2) のように自分で乗算展開した場合はかなり遅くなっています。
NDK の Cコンパイラよりも通常演算の最適化がまだ弱いようです。

// RenderScript: (1)
rs_matrix4x4    TransformMatrix;
void root( const float4* vin, float4* vout )
{
    *vout= rsMatrixMultiply( &TransformMatrix, *vin );
}

// RenderScript: (2)
rs_matrix4x4    TransformMatrix;
void root( const float4* vin, float4* vout )
{
    vout->x= TransformMatrix.m[ 0] * vin->x
            +TransformMatrix.m[ 4] * vin->y
            +TransformMatrix.m[ 8] * vin->z
            +TransformMatrix.m[12] * vin->w;

    vout->y= TransformMatrix.m[ 1] * vin->x
            +TransformMatrix.m[ 5] * vin->y
            +TransformMatrix.m[ 9] * vin->z
            +TransformMatrix.m[13] * vin->w;

    vout->z= TransformMatrix.m[ 2] * vin->x
            +TransformMatrix.m[ 6] * vin->y
            +TransformMatrix.m[10] * vin->z
            +TransformMatrix.m[14] * vin->w;

    vout->w= TransformMatrix.m[ 3] * vin->x
            +TransformMatrix.m[ 7] * vin->y
            +TransformMatrix.m[11] * vin->z
            +TransformMatrix.m[15] * vin->w;
}

●Java

(3) が予想よりも高速でした。
演算自体の負荷が高い場合は比較的言語の差が出にくいのですが、
JIT が効果的に働いているのではないかと思われます。
結果を Java で受け取るならば、データ転送時間が不要な分もっと
差が縮まる可能性があります。

// java : (3)
float[] fv= transformMatrix.getArray();
float[] destbuf= new float[MAX_TRANSFORM*4];

long    stime= System.currentTimeMillis();
for( int i= 0 ; i< MAX_TRANSFORM ; i++ ){
    int base= i*4;
    destbuf[base+0]=
          fv[ 0] * srcbuf[base+0]
        + fv[ 4] * srcbuf[base+1]
        + fv[ 8] * srcbuf[base+2]
        + fv[12] * srcbuf[base+3];

    destbuf[base+1]=
          fv[ 1] * srcbuf[base+0]
        + fv[ 5] * srcbuf[base+1]
        + fv[ 9] * srcbuf[base+2]
        + fv[13] * srcbuf[base+3];

    destbuf[base+2]=
          fv[ 2] * srcbuf[base+0]
        + fv[ 6] * srcbuf[base+1]
        + fv[10] * srcbuf[base+2]
        + fv[14] * srcbuf[base+3];

    destbuf[base+3]=
          fv[ 3] * srcbuf[base+0]
        + fv[ 7] * srcbuf[base+1]
        + fv[11] * srcbuf[base+2]
        + fv[15] * srcbuf[base+3];
}

(4) は Android に含まれるライブラリを利用したものです。
最も低速でした。

// java : (4)
for( int i= 0 ; i< MAX_TRANSFORM ; i++ ){
    int base= i*4;
    Matrix.multiplyMV( destbuf, base, fv, 0, srcbuf, base );
}

●NDK

RenderScript (1) にかなり近い数値でした。
debug build ではこの半分の速度だったので release build の最適化が
かなり効いているようです。
展開コードを調べると、fmuls + fadds の組み合わせが最適化により
fmacs の積和命令に置き換わっていることがわかります。

// NDK C++ : (6)
static void loopndk2( const Vect4* vin, Vect4* vout, int length )
{
    for( int i= 0 ; i< length ; i++ ){
        vout->x= TransformMatrix._11 * vin->x
                +TransformMatrix._21 * vin->y
                +TransformMatrix._31 * vin->z
                +TransformMatrix._41 * vin->w;
        vout->y= TransformMatrix._12 * vin->x
                +TransformMatrix._22 * vin->y
                +TransformMatrix._32 * vin->z
                +TransformMatrix._42 * vin->w;
        vout->z= TransformMatrix._13 * vin->x
                +TransformMatrix._23 * vin->y
                +TransformMatrix._33 * vin->z
                +TransformMatrix._43 * vin->w;
        vout->w= TransformMatrix._14 * vin->x
                +TransformMatrix._24 * vin->y
                +TransformMatrix._34 * vin->z
                +TransformMatrix._44 * vin->w;
        vin++;
        vout++;
    }
}

試しにアセンブラで記述してみたのが (7)。
やはり NEON はなくても fmacs が効果的です。
さらに最速ケースを調べるためにスレッド化し、Cortex-A9 2 core を使って
並列実行したのが (8) です。

// NDK asm : (7)/(8)
    __asm__ __volatile( "\
    1: \n\
        vldmia %1!, {d8-d9} \n\
        vldmia %0, {d0-d7} \n\
        fmuls s20,s0 ,s16 \n\
        fmuls s21,s4 ,s16 \n\
        fmuls s22,s8 ,s16 \n\
        fmuls s23,s12,s16 \n\
        fmacs s20,s1 ,s17 \n\
        fmacs s21,s5 ,s17 \n\
        fmacs s22,s9 ,s17 \n\
        fmacs s23,s13,s17 \n\
        fmacs s20,s2 ,s18 \n\
        fmacs s21,s6 ,s18 \n\
        fmacs s22,s10,s18 \n\
        fmacs s23,s14,s18 \n\
        fmacs s20,s3 ,s19 \n\
        fmacs s21,s7 ,s19 \n\
        fmacs s22,s11,s19 \n\
        fmacs s23,s15,s19 \n\
        vstmia %2!, {d10-d11} \n\
        subs  %3,%3,#1 \n\
        bne 1b \n\
    "
    : "=&r"( mat ), "=&r"( vin ), "=&r"( vout ), "=&r"( length )
    : "0"( mat ), "1"( vin ), "2"( vout ), "3"( length )
    :
        "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7",
        "d8", "d9", "d10", "d11", "cc"
    );

以上より

RenderScript(LLVM)
 ・Single Thread 実行
 ・rsMatrixMultiply() は内部で fmacs (積和) 命令を使用し最適化されている
 ・RenderScript は通常の演算は fmacs (積和) に展開しない

NDK(gcc)
 ・通常の演算コードも fmacs (積和) に最適化される

おそらく NEON があれば rsMatrixMultiply() が更に高速で、コンパイラが
展開したコードよりも高速に実行できたのではないかと考えられます。

●まとめ

SIMD がなくマルチコアも活用されないので、そのまま NDK C++ で書いた
コードと大差ない結果となってしまいました。
今後 Android OS 4.0 (Ice Cream Sandwich) が登場し、さらに多くの
デバイスで走るようになれば面白いデータが見られるのではないかと思います。

当然ながら、何でもできる NDK が最も速度を引き出せることには変わりありません。
その代わり全部自分でやらなければなりません。
armeabi, arameabi-v7a, neon, x86 等のアーキテクチャ対応、
SIMD 対応、スレッド化などすべてに取り組むのは大変です。

RenderScript はアーキテクチャを選ばず、バイナリ化してもランタイムの
改良で性能が向上する可能性があります。
これまで使ってきたように Java から気軽に呼び出せるため
Android 4.0 が普及すれば色々と使い道が増えると思います。

関連エントリ
Android 3.x RenderScript (6) Graphics まとめ
Android 3.x RenderScript (5) 任意の 3D モデルを描画する
Android 3.x RenderScript (4) script で頂点を書き換える
Android 3.x RenderScript (3) 独自シェーダーの割り当てとメッシュの描画(2D)
Android 3.x RenderScript (2) 描画と Allocation
Android 3.x RenderScript (1)