Raspberry Piのベクトル演算の実力(2012/08/19)

Raspberry Pi が使っているCPU (ARM1176JZFS) は浮動小数点プロセッサとしてVFPv2を持っています。このVFPv2は通常のスカラーモードに加えて、ベクトル演算を効率的に行うモードを持っています。Cコンパイラはスカラーモードのみを使うコードを生成しますが、ベクトル演算モードで高速化されるかどうかを調べてみました。

下のCのプログラムは4x4の行列の1000万回の乗算について、Cで書いた関数 (MulMatrix4) とベクトル演算モードを使うようにアセンブラで書いた関数 (MulMatrix4x4asm) の実行時間を比較するものです。アセンブラで書いた関数はCのプログラムにリンクして利用しています。 時間の計測には gettimeofday を使っています。

//---------------------------------------------------------------------------
// Multiply 4x4 Matrix
//   2012/08/19 Jun Mizutani
// as -mcpu=arm1176jzf-s -mfpu=vfp mat4x4.s -o mat4x4asm.o
// gcc -Wall -o mat4 mat4.c mat4x4asm.o
// gcc -O2 -Wall -o mat4-O2 mat4.c mat4x4asm.o
//---------------------------------------------------------------------------

#include <stdio.h>
#include <sys/time.h>

typedef struct Matrix4 {
    double m0,m1,m2,m3, m4,m5,m6,m7, m8,m9,m10,m11, m12,m13,m14,m15;
} Matrix4;

Matrix4 a = { 1, 2, 3, 4,  1, 2, 3, 4,  1, 2, 3, 4,     1, 2, 3, 4 };
Matrix4 b = { 0, 1, 2, 3,  4, 5, 6, 7,  8, 9, 10, 11,  12, 13, 14, 15 };
Matrix4 c = { 0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,     0, 0, 0, 0 };

extern void MulMatrix4x4asm(Matrix4 *result, Matrix4 *a, Matrix4 *b);

//---------------------------------------------------------------------------
//  Multiply Matrix  C = A * B
//---------------------------------------------------------------------------
void MulMatrix4(Matrix4 *a, Matrix4 *b, Matrix4 *c)
{
    c->m0 = a->m0 * b->m0 + a->m4 * b->m1 + a->m8 * b->m2 + a->m12 * b->m3;
    c->m1 = a->m1 * b->m0 + a->m5 * b->m1 + a->m9 * b->m2 + a->m13 * b->m3;
    c->m2 = a->m2 * b->m0 + a->m6 * b->m1 + a->m10* b->m2 + a->m14 * b->m3;
    c->m3 = a->m3 * b->m0 + a->m7 * b->m1 + a->m11* b->m2 + a->m15 * b->m3;

    c->m4 = a->m0 * b->m4 + a->m4 * b->m5 + a->m8 * b->m6 + a->m12 * b->m7;
    c->m5 = a->m1 * b->m4 + a->m5 * b->m5 + a->m9 * b->m6 + a->m13 * b->m7;
    c->m6 = a->m2 * b->m4 + a->m6 * b->m5 + a->m10* b->m6 + a->m14 * b->m7;
    c->m7 = a->m3 * b->m4 + a->m7 * b->m5 + a->m11* b->m6 + a->m15 * b->m7;

    c->m8 = a->m0 * b->m8 + a->m4 * b->m9 + a->m8 * b->m10+ a->m12 * b->m11;
    c->m9 = a->m1 * b->m8 + a->m5 * b->m9 + a->m9 * b->m10+ a->m13 * b->m11;
    c->m10= a->m2 * b->m8 + a->m6 * b->m9 + a->m10* b->m10+ a->m14 * b->m11;
    c->m11= a->m3 * b->m8 + a->m7 * b->m9 + a->m11* b->m10+ a->m15 * b->m11;

    c->m12= a->m0 * b->m12+ a->m4 * b->m13+ a->m8 * b->m14+ a->m12 * b->m15;
    c->m13= a->m1 * b->m12+ a->m5 * b->m13+ a->m9 * b->m14+ a->m13 * b->m15;
    c->m14= a->m2 * b->m12+ a->m6 * b->m13+ a->m10* b->m14+ a->m14 * b->m15;
    c->m15= a->m3 * b->m12+ a->m7 * b->m13+ a->m11* b->m14+ a->m15 * b->m15;
}

void PrintMatrix4(Matrix4 *a)
{
    printf("%10.5e %10.5e %10.5e %10.5e \n", a->m0, a->m4, a->m8,  a->m12);
    printf("%10.5e %10.5e %10.5e %10.5e \n", a->m1, a->m5, a->m9,  a->m13);
    printf("%10.5e %10.5e %10.5e %10.5e \n", a->m2, a->m6, a->m10, a->m14);
    printf("%10.5e %10.5e %10.5e %10.5e \n\n", a->m3, a->m7, a->m11, a->m15);
}

int main(int argc, char* argv[])
{
  struct timeval start, now;
  struct timezone tzone;
  double elapsedTime = 0.0;
  int i;

  printf("start C version\n");
  gettimeofday(&start, &tzone);

  for(i=0; i<10000000; i++) MulMatrix4(&a, &b, &c);

  gettimeofday(&now, &tzone);
  elapsedTime = (double)(now.tv_sec - start.tv_sec) +
    (double)(now.tv_usec - start.tv_usec)/1000000.0;
  printf("%4.6f sec\n", elapsedTime);

  PrintMatrix4(&a);
  PrintMatrix4(&b);
  PrintMatrix4(&c);

  printf("start ASM version\n");
  gettimeofday(&start, &tzone);

  for(i=0; i<10000000; i++) MulMatrix4x4asm(&c, &a, &b);

  gettimeofday(&now, &tzone);
  elapsedTime = (double)(now.tv_sec - start.tv_sec) +
    (double)(now.tv_usec - start.tv_usec)/1000000.0;
  printf("%4.6f sec\n", elapsedTime);

  PrintMatrix4(&c);

  return 0;
}

VFPv2のベクトルモードを使った行列の乗算

ベクトル演算モードを使うようにアセンブラで書いた4x4の行列の乗算を行うコードです。この関数にはポインタとして各行列の先頭アドレスを引数として渡されるため、アセンブリコード側では r0、r1、r2 で行列の先頭アドレスを受け取ることができます。r1の行列とr2の行列を乗算した結果が r0 に返されます。最初に FPSTR を書き換えてベクトル演算モード(4つの演算を同時に行う設定)に移行し、乗算終了後にスカラーモードに戻しています。fldmiadで4つのレジスタに倍精度浮動小数点数を一命令でロードし、同様にfstmiadで4つのレジスタの倍精度浮動小数点数をメモリに書き出しています。ベクトル演算モードを使うと4x4行列の乗算が非常にコンパクトに記述できます。

@----------------------------------------------------------
@ mat4x4.s   2012/08/19 Jun Mizutani 
@ as -mcpu=arm1176jzf-s -mfpu=vfp mat4x4.s -o mat4x4asm.o
@ gcc -O2 -Wall -o mat4 mat4.c mat4x4asm.o
@---------------------------------------------------------

.text
.global MulMatrix4x4asm

@ MulMatrix4x4asm(Matrix4 *result, Matrix4 *a, Matrix4 *b)
@ [result] = [a] x [b]
@ [  r0  ] = [r1] x [r2]
        .align  3
MulMatrix4x4asm:
        stmfd   sp!, {r0-r3, lr}
        vpush   {d0-d15}
        bl      vector4_mode            @ LEN=4: vector mode
        mov     r3, r0
        fldmiad r2!, {d0-d3}            @ vec_e
        mov     r0, r1                  @ vec_a
        fldmiad r0!, {d8-d11}           @ load a[0_3]
        fmuld   d4, d8, d0              @ w[i] =        a[i] * e0
        fldmiad r0!, {d12-d15}          @ load b[0-3]
        fmacd   d4, d12, d1             @ w[i] = w[i] + b[i] * e1
        fldmiad r0!, {d8-d11}           @ load c[0-3]
        fmacd   d4, d8, d2              @ w[i] = w[i] + c[i] * e2
        fldmiad r0, {d12-d15}           @ load d[0-3]
        fmacd   d4, d12, d3             @ w[i] = w[i] + d[i] * e3
        fstmiad r3!, {d4-d7}            @ store w[i]
        fldmiad r2!, {d0-d3}            @ vec_f
        mov     r0, r1                  @ vec_a
        fldmiad r0!, {d8-d11}
        fmuld   d4, d8, d0              @ x[i] =        a[i] * f0
        fldmiad r0!, {d12-d15}
        fmacd   d4, d12, d1             @ x[i] = x[i] + b[i] * f1
        fldmiad r0!, {d8-d11}
        fmacd   d4, d8, d2              @ x[i] = x[i] + c[i] * f2
        fldmiad r0, {d12-d15}
        fmacd   d4, d12, d3             @ x[i] = x[i] + d[i] * f3
        fstmiad r3!, {d4-d7}            @ store x[i]
        fldmiad r2!, {d0-d3}            @ vec_g
        mov     r0, r1                  @ vec_a
        fldmiad r0!, {d8-d11}
        fmuld   d4, d8, d0              @ y[i] =        a[i] * g0
        fldmiad r0!, {d12-d15}
        fmacd   d4, d12, d1             @ y[i] = y[i] + b[i] * g1
        fldmiad r0!, {d8-d11}
        fmacd   d4, d8, d2              @ y[i] = y[i] + c[i] * g2
        fldmiad r0, {d12-d15}
        fmacd   d4, d12, d3             @ y[i] = y[i] + d[i] * g3
        fstmiad r3!, {d4-d7}            @ store y[i]
        fldmiad r2, {d0-d3}             @ vec_h
        mov     r0, r1                  @ vec_a
        fldmiad r0!, {d8-d11}
        fmuld   d4, d8, d0              @ z[i] =        a[i] * h0
        fldmiad r0!, {d12-d15}
        fmacd   d4, d12, d1             @ z[i] = z[i] + b[i] * h1
        fldmiad r0!, {d8-d11}
        fmacd   d4, d8, d2              @ z[i] = z[i] + c[i] * h2
        fldmiad r0, {d12-d15}
        fmacd   d4, d12, d3             @ z[i] = z[i] + d[i] * h3
        fstmiad r3, {d4-d7}

        bl      scalar_mode             @ LEN=1: scalar mode
        vpop    {d0-d15}
        ldmfd   sp!, {r0-r3, pc}

@-------------------------------------------------------------------------
@ set FPSTR STRIDE[21:20] and LEN[18:16]
@-------------------------------------------------------------------------
scalar_mode:
        stmfd   sp!, {r2, lr}
        fmrx    r2, FPSCR              @ FPSCR --> r10
        bic     r2, r2, #0x00370000    @ clear (STRIDE=1 and LEN=1)
        fmxr    FPSCR, r2              @ r10 --> FPSCR
        ldmfd   sp!, {r2, pc}

vector4_mode:
        stmfd   sp!, {r2, lr}
        fmrx    r2, FPSCR              @ FPSCR --> r10
        bic     r2, r2, #0x00370000    @ clear (STRIDE=1 and LEN=1)
        orr     r2, r2, #0x00030000    @ set bit[17:16] (STRIDE=1 and LEN=4)
        fmxr    FPSCR, r2              @ r10 --> FPSCR
        ldmfd   sp!, {r2, pc}

実行

最適化なしでコンパイル

最適化なしでコンパイルした場合の実行結果を示します。アセンブラのルーチンは3.5倍速く実行されました。

$ as -mcpu=arm1176jzf-s -mfpu=vfp mat4x4.s -o mat4x4asm.o
$ gcc -Wall -o mat4 mat4.c mat4x4asm.o
$ ./mat4
start C version
23.145123 sec
1.00000e+00 1.00000e+00 1.00000e+00 1.00000e+00 
2.00000e+00 2.00000e+00 2.00000e+00 2.00000e+00 
3.00000e+00 3.00000e+00 3.00000e+00 3.00000e+00 
4.00000e+00 4.00000e+00 4.00000e+00 4.00000e+00 

0.00000e+00 4.00000e+00 8.00000e+00 1.20000e+01 
1.00000e+00 5.00000e+00 9.00000e+00 1.30000e+01 
2.00000e+00 6.00000e+00 1.00000e+01 1.40000e+01 
3.00000e+00 7.00000e+00 1.10000e+01 1.50000e+01 

6.00000e+00 2.20000e+01 3.80000e+01 5.40000e+01 
1.20000e+01 4.40000e+01 7.60000e+01 1.08000e+02 
1.80000e+01 6.60000e+01 1.14000e+02 1.62000e+02 
2.40000e+01 8.80000e+01 1.52000e+02 2.16000e+02 

start ASM version
6.567551 sec
6.00000e+00 2.20000e+01 3.80000e+01 5.40000e+01 
1.20000e+01 4.40000e+01 7.60000e+01 1.08000e+02 
1.80000e+01 6.60000e+01 1.14000e+02 1.62000e+02 
2.40000e+01 8.80000e+01 1.52000e+02 2.16000e+02 

最適化(-O2)でコンパイル

$ as -mcpu=arm1176jzf-s -mfpu=vfp mat4x4.s -o mat4x4asm.o
$ gcc -Wall -O2 -o mat4 mat4.c mat4x4asm.o
$ ./mat4-O2 
start C version
6.730308 sec
1.00000e+00 1.00000e+00 1.00000e+00 1.00000e+00 
2.00000e+00 2.00000e+00 2.00000e+00 2.00000e+00 
3.00000e+00 3.00000e+00 3.00000e+00 3.00000e+00 
4.00000e+00 4.00000e+00 4.00000e+00 4.00000e+00 

0.00000e+00 4.00000e+00 8.00000e+00 1.20000e+01 
1.00000e+00 5.00000e+00 9.00000e+00 1.30000e+01 
2.00000e+00 6.00000e+00 1.00000e+01 1.40000e+01 
3.00000e+00 7.00000e+00 1.10000e+01 1.50000e+01 

6.00000e+00 2.20000e+01 3.80000e+01 5.40000e+01 
1.20000e+01 4.40000e+01 7.60000e+01 1.08000e+02 
1.80000e+01 6.60000e+01 1.14000e+02 1.62000e+02 
2.40000e+01 8.80000e+01 1.52000e+02 2.16000e+02 

start ASM version
6.400080 sec
6.00000e+00 2.20000e+01 3.80000e+01 5.40000e+01 
1.20000e+01 4.40000e+01 7.60000e+01 1.08000e+02 
1.80000e+01 6.60000e+01 1.14000e+02 1.62000e+02 
2.40000e+01 8.80000e+01 1.52000e+02 2.16000e+02 

結果

VFPv2のベクトル演算モードをアセンブリで書いて使用しても、わずかに速くなるだけでコンパイラの最適化とほとんど差がでない結果になりました。バイナリのサイズは6分の1程度に小さく出来ますが、高速化のためにアセンブリを使う必要はありませんね。ちょっと残念。

しかし、4x4の行列を1000万回乗算しても6秒程度なので、安価なRaspberry PiでもOpenGL ES2.0 を使って十分に高速な3次元のアプリケーションの作成が可能であると思います。


続く...