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)でコンパイル
- O2 で最適化を指定してコンパイルした場合の実行結果を示します。最適化でCの関数は非常に速くなり、ベクトル演算モードを使っても5%ほど速くなるだけでした。
$ 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次元のアプリケーションの作成が可能であると思います。