Raspberry Pi と Raspberry Pi2 Model B の互換性 (2015/02/16)
今回はアセンブラを使って、Raspberry Pi と Raspberry Pi2 の細かい違いを見てみます。 シングルコアでクロックが 700 MHz の ARM1176JZF-S(BCM2835) を使った Raspberry Pi から、Raspberry Pi2 ではクアッドコアで 900MHz の Cortex-A7 (BCM2836) にCPU が変更されました。CPUコアの個数が4倍になって、クロックが約30%速くなっていますが、それ以外に何が変わっているのでしょうか。 Raspberry Pi用のアプリケーションを Raspberry Pi2 で動作させる場合の互換性は、公式サイトでも「Complete compatibility with Raspberry Pi 1」のように 初代 Raspberry Pi と Raspberry Pi2 では完全互換であるといっています。
ボード | Raspberry Pi | Raspberry Pi2 |
---|---|---|
チップ名称 | BCM2835 | BCM2836 |
基本命令セット | ARMv6 | ARMv7 |
コア名称 | ARM1176JZF-S | Cortex-A7 |
コア数 | 1 | 4 |
浮動小数点演算 | VFPv2 | VFPv4 + NEON |
クロック | 700MHz | 900MHz |
メモリ | 512MB | 1GB |
互換性と言った面では、浮動小数点演算部が VFPv2 と VFPv4 + NEON であることが大きな違いです。 VFPv2で使えたベクトル演算モードがVFPv4 で使えなくなった一方、より強力な NEON が Raspberry Pi2 には搭載されています。私が試した限りでは、浮動小数点演算もCで書いたソースのコンパイルオプションをCortex-A7に特化させても、初代Pi用のバイナリから大きく高速化されることもありませんでした。 メモリが増えて、コアが増えて速くなっただけというのも面白くありません。 細かいところを調べてみたいと思います。
前に各種言語のベンチマークで作成したコード は VFPv2 のベクトル演算モードを使っていますが、Raspberry Pi2で実行してみます。
$ ./mulmat-O2 start C version 0.609153 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 start ASM version 60.811646 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
Raspberry Pi2 ではベクトル演算モードを使うコードは実行できないかと思ったのですが、カーネルが対応しているのか、非常に遅いですが実行できました。 アセンブラ版は初代Raspberry Piで 0.63秒程度 なので、100倍近い時間がかかっています。
4x4正方行列の乗算
Raspberry Pi2 のCPU Cortex-A7 にはベクトル演算用のNEONと呼ばれる命令セットが含まれています。 初代Raspberry PiにはNEONは含まれていません。 NEON は 128ビットのレジスタを16本(q0 - q15)持ち、これらは64ビットのレジスタ32本(d0 - d31) としても使用できます。 qレジスタに単精度浮動小数点数(32ビット)を4つ格納して、同時に4つの演算が可能となっています。 いつも同じネタですが、NEON のベクトル演算命令を使って4x4正方行列の乗算を行ってRaspberry Pi2の性能を評価してみます。
計算方法
行列の要素が、次のように列優先(縦方向に並ぶ)で格納されている場合の m = a * b という乗算を考えます。例えば行列 a では要素がメモリにa0, a1, a2 のように順に格納されているとします。 C言語で float の配列となり、a を配列名とすると各要素は a[0]、a[1]、a[2] となります。
| b0 b4 b8 b12 | b1 b5 b9 b13 | b2 b6 b10 b14 | b3 b7 b11 b15 --------------------------+------------------------- a0 a4 a8 a12 | m0 m4 m8 m12 a1 a5 a9 a13 | m1 m5 m9 m13 a2 a6 a10 a14 | m2 m6 m10 m14 a3 a7 a11 a15 | m3 m7 m11 m15
m, a, b が 4x4行列の場合、行列の積 m = a * b は次のように計算します。
m0 = a0*b0 + a4*b1 + a8*b2 + a12*b3 m1 = a1*b0 + a5*b1 + a9*b2 + a13*b3 m2 = a2*b0 + a6*b1 + a10*b2 + a14*b3 m3 = a3*b0 + a7*b1 + a11*b2 + a15*b3 m4 = a0*b4 + a4*b5 + a8*b6 + a12*b7 m5 = a1*b4 + a5*b5 + a9*b6 + a13*b7 m6 = a2*b4 + a6*b5 + a10*b6 + a14*b7 m7 = a3*b4 + a7*b5 + a11*b6 + a15*b7 m8 = a0*b8 + a4*b9 + a8*b10 + a12*b14 m9 = a1*b8 + a5*b9 + a9*b10 + a13*b14 m10 = a2*b8 + a6*b9 + a10*b10 + a14*b14 m11 = a3*b8 + a7*b9 + a11*b10 + a15*b14 m12 = a0*b12 + a4*b13 + a8*b14 + a12*b15 m13 = a1*b12 + a5*b13 + a9*b14 + a13*b15 m14 = a2*b12 + a6*b13 + a10*b14 + a14*b15 m15 = a3*b12 + a7*b13 + a11*b14 + a15*b15
NEON のレジスタ
NEONは、128ビット幅の16本のレジスタ (クアッドワードレジスタ : q0 - q15) を持っていて、これを64ビットレジスタ32本、32ビットレジスタ32本 (ダブルワードレジスタ : d0 - d31) としてアクセスすることができます。 メモリから128ビット(16バイト)を1命令で読み書きできるため、8個の単精度浮動小数点数を一括してメモリから読み出し、4つの単精度浮動小数点数を並列で演算、8個の単精度浮動小数点数をメモリに格納、といった効率的な動作が可能です。
f32 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | | | | | | | | d0[0] | +-------+-------+-------+-------+-------+-------+-------+-------+ | | | d1 | d0 (64) | d0 - d31 +---------------+---------------+---------------+---------------+ | q1 | q0 (128bit) | q0 - q16 +-------------------------------+-------------------------------+
NEONで計算
さて、上記の4x4正方行列の乗算を NEON のアセンブラで計算することにします。まず、行列a と行列b をNEONの128ビットのレジスタに設定します。 行列a と行列b がそれぞれ16個の要素を持つ配列として格納されている場合、要素が4つの列ベクトルを1つのクアッドワードレジスタに格納します。行列 a は q4 から q7 の4つのクアッドワードレジスタに格納します。q4 から格納する理由は、行列 b を q0 から q3 に格納したい (理由は後述) ためです。
q4 = { a0, a1, a2, a3} q5 = { a4, a5, a6, a7} q6 = { a8, a9, a10, a11} q7 = {a12, a13, a14, a15}
行列データの読み込み
行列 a の先頭要素が格納されているメモリの先頭アドレスが r1 レジスタに設定されている場合、以下の2つの命令で NEONの q4 から q7 レジスタに行列の16個の要素を格納することができます。
vld1.32 {d8-d11}, [r1]! vld1.32 {d12-d15}, [r1]!
行列b を q0 から格納するのは、0から始まっているほうが単精度浮動小数点数を d0[0] のように指定しやすいためです。 q0 レジスタをダブルワードレジスタ d0, d1 として2つに分けて扱い、さらに d0 を d0[0] と d0[1] に分けることで単精度浮動小数点数(f32)の要素を個別にアクセスできます。 例えば行列a の要素を指定するには d8[0] から d15[1] となります。分かり難いですね。
q0 = { b0, b1, b2, b3} q1 = { b4, b5, b6, b7} q2 = { b8, b9, b10, b11} q3 = {b12, b13, b14, b15}
行列 a と同じく、行列 b の先頭要素が格納されているメモリの先頭アドレスが r2 レジスタに設定されている場合、以下の命令で NEONの q0 から q3 に各要素を格納できます。
vld1.32 {d0-d3}, [r2]! vld1.32 {d4-d7}, [r2]!
演算結果の行列の積は q8 - q11 に格納することにします。
q8 = { m0, m1, m2, m3} q9 = { m4, m5, m6, m7} q10 = { m8, m9, m10, m11} q11 = {m12, m13, m14, m15}
乗算の実行
クアッドワードレジスタに格納された4つの単精度浮動小数点数は、1命令でそれぞれの要素に1つの単精度浮動小数点数を乗算することができます。a0, a1, a2, a3 に対して b0 という数値を乗算して m0, m1, m2, m3 に格納するためには vmul.f32 という命令を実行します。ARM のアセンブラでは @ より後、行末までコメントになります。
@ q8 = {a0*b0, a1*b0, a2*b0, a3*b0}
vmul.f32 q8, q4, d0[0]
つぎに、a4, a5, a6, a7 に対して b1 を乗算して、それぞれを q8 の内容に加算するには vmla.f32 という命令を使用します。 vmul と vmla の違いは、先頭に指定したレジスタに結果を単に格納 (vmul) するか、もともと入っていた値に結果を加算 (vmla) するかの違いです。
@ q8 = {a0*b0+a4*b1, a1*b0+a5*b1, a2*b0+a6*b1, a3*b0+a7*b1}
vmla.f32 q8, q5, d0[1]
さらに続けて、a8, a9, a10, a11 に対して b2 を乗算して、それぞれを q8 の内容に加算します。 ここでも vmla.f32 という命令を使用します。
@ q8 = {a0*b0+a4*b1+a8*b2, a1*b0+a5*b1+a9*b2, a2*b0+a6*b1+a10*b2, ...}
vmla.f32 q8, q6, d1[0]
最後に、a12, a13, a14, a15 に対して b3 を乗算して、それぞれを q8 の内容に加算します。 これで m1 から m3 の計算は終了です。
@ q8 = {a0*b0+a4*b1+a8*b2+a12*b3, a1*b0+a5*b1+a9*b2+a13*b3, ...}
vmla.f32 q8, q7, d1[1]
同様に m4, m5, m6, m7 を計算します。b4 は d2[0] としてアクセスしています。
@ q9 = {a0*b4, a1*b4, a2*b4, a3*b4} vmul.f32 q9, q4, d2[0] @ q9 = {a0*b4+a4*b5, a1*b4+a5*b5, a2*b4+a6*b5, a3*b4+a7*b5} vmla.f32 q9, q5, d2[1] @ q9 = {a0*b4+a4*b5+a8*b6, a1*b4+a5*b5+a9*b6, a2*b4+a6*b5+a10*b6, ...} vmla.f32 q9, q6, d3[0] @ q9 = {a0*b4+a4*b5+a8*b6+a12*b7, a1*b4+a5*b5+a9*b6+a13*b7, ... } vmla.f32 q9, q7, d3[1]
同様に m8, m9, m10, m11 を計算します。b8 は d4[0] としてアクセスできます。
@ q10 = {a0*b8, a1*b8, a2*b8, a3*b8} vmul.f32 q10, q4, d4[0] @ q10 = {a0*b8+a4*b9, a1*b8+a5*b9, a2*b8+a6*b9, a3*b8+a7*b9} vmla.f32 q10, q5, d4[1] @ q10 = {a0*b8+a4*b9+a8*b10, a1*b8+a5*b9+a9*b10, a2*b8+a6*b9+a10*b10, ... } vmla.f32 q10, q6, d5[0] @ q10 = {a0*b8+a4*b9+a8*b10+a12*b7, a1*b8+a5*b9+a9*b10+a13*b11, ... } vmla.f32 q10, q7, d5[1]
最後に m12, m13, m14, m15 を計算します。b12 は d6[0] としてアクセスできます。
@ q11 = {a0*b12, a1*b12, a2*b12, a3*b12} vmul.f32 q11, q4, d6[0] @ q11 = {a0*b12+a4*b13, a1*b12+a5*b13, a2*b12+a6*b13, a3*b12+a7*b13} vmla.f32 q11, q5, d6[1] @ q11 = {a0*b12+a4*b13+a8*b14, a1*b12+a5*b13+a9*b14, ... } vmla.f32 q11, q6, d7[0] @ q11 = {a0*b12+a4*b13+a8*b14+a12*b7, a1*b12+a5*b13+a9*b14+a13*b15, ... } vmla.f32 q11, q7, d7[1]
結果のメモリへの格納
q8 - q11 に格納された行列の積を r0 でアドレスを指定されたメモリに書き戻します。
vst1.32 {d16-d19}, [r0]! vst1.32 {d20-d23}, [r0]!
以上で 4x4正方行列 2つをメモリから読み出し、乗算した結果をメモリに書き出しました。
ソース
neonvmul.s
上で解説した NEON を使って4x4行列の乗算を行うアセンブリコードです。かなりコンパクトに記述できるのが分かります。
@----------------------------------------------- @ neonvmul.s @----------------------------------------------- .text .align 2 .global Matrix4x4Mul @ Matrix4x4Mul(Matrix4 *result, Matrix4 *a, Matrix4 *b) @ [result] = [a] x [b] @ [ r0 ] = [r1] x [r2] Matrix4x4Mul: vld1.32 {d8-d11}, [r1]! @ r1 : a vld1.32 {d12-d15}, [r1]! vld1.32 {d0-d3}, [r2]! @ r2 : b vld1.32 {d4-d7}, [r2]! vmul.f32 q8, q4, d0[0] vmla.f32 q8, q5, d0[1] vmla.f32 q8, q6, d1[0] vmla.f32 q8, q7, d1[1] vmul.f32 q9, q4, d2[0] vmla.f32 q9, q5, d2[1] vmla.f32 q9, q6, d3[0] vmla.f32 q9, q7, d3[1] vmul.f32 q10, q4, d4[0] vmla.f32 q10, q5, d4[1] vmla.f32 q10, q6, d5[0] vmla.f32 q10, q7, d5[1] vmul.f32 q11, q4, d6[0] vmla.f32 q11, q5, d6[1] vmla.f32 q11, q6, d7[0] vmla.f32 q11, q7, d7[1] vst1.32 {d16-d19}, [r0]! @ r0 : m vst1.32 {d20-d23}, [r0]! mov pc, lr @ return
neonvmul2.s
上記のコードでは vmul.f32 と vmla.f32 命令を見ると、結果を書き込む先となるレジスタ (q8 - q9) が同じものが並んでいます。 例えば上の赤く色分けした q8 です。書き込み先のレジスタに書き込みが終わらないと、次の命令を実行できないため待機させられ、結果として遅くなります。 次のコードでは命令の順序を変えてレジスタが4つおきに書き込まれるようにしています。これでさらに高速化されることが期待できます。
@----------------------------------------------- @ neonvmul2.s @----------------------------------------------- .text .global Matrix4x4Mul2 .align 2 @ Matrix4x4Mul2(Matrix4 *result, Matrix4 *a, Matrix4 *b) @ [result] = [a] x [b] @ [ r0 ] = [r1] x [r2] Matrix4x4Mul2: vld1.32 {d8-d11}, [r1]! @ r1 : a vld1.32 {d12-d15}, [r1]! vld1.32 {d0-d3}, [r2]! @ r2 : b vld1.32 {d4-d7}, [r2]! vmul.f32 q8, q4, d0[0] vmul.f32 q9, q4, d2[0] vmul.f32 q10, q4, d4[0] vmul.f32 q11, q4, d6[0] vmla.f32 q8, q5, d0[1] vmla.f32 q9, q5, d2[1] vmla.f32 q10, q5, d4[1] vmla.f32 q11, q5, d6[1] vmla.f32 q8, q6, d1[0] vmla.f32 q9, q6, d3[0] vmla.f32 q10, q6, d5[0] vmla.f32 q11, q6, d7[0] vmla.f32 q8, q7, d1[1] vmla.f32 q9, q7, d3[1] vmla.f32 q10, q7, d5[1] vmla.f32 q11, q7, d7[1] vst1.32 {d16-d19}, [r0]! @ r0 : m vst1.32 {d20-d23}, [r0]! mov pc, lr @ return
mat4n.c
アセンブリのコードとリンクして実行する C のメインプログラムです。
//--------------------------------------------------------------------------- // Multiply 4x4 Matrix // 2015/02/15 Jun Mizutani // as -mcpu=cortex-a7 -mfpu=neon-vfpv4 neonvmul.s -o neonvmul.o // as -mcpu=cortex-a7 -mfpu=neon-vfpv4 neonvmul2.s -o neonvmul2.o // gcc -O2 -mfloat-abi=hard -march=armv7-a -mfpu=neon-vfpv4 -o mat4n mat4n.c neonvmul.o neonvmul2.o //--------------------------------------------------------------------------- #include <stdio.h> #include <sys/time.h> typedef struct Matrix4 { float 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 Matrix4x4Mul(Matrix4 *result, Matrix4 *a, Matrix4 *b); extern void Matrix4x4Mul2(Matrix4 *result, Matrix4 *a, Matrix4 *b); void MulMatrix4( Matrix4 *r, Matrix4 *a, Matrix4 *b) { r->m0 = a->m0 * b->m0 + a->m4 * b->m1 + a->m8 * b->m2 + a->m12 * b->m3; r->m1 = a->m1 * b->m0 + a->m5 * b->m1 + a->m9 * b->m2 + a->m13 * b->m3; r->m2 = a->m2 * b->m0 + a->m6 * b->m1 + a->m10* b->m2 + a->m14 * b->m3; r->m3 = a->m3 * b->m0 + a->m7 * b->m1 + a->m11* b->m2 + a->m15 * b->m3; r->m4 = a->m0 * b->m4 + a->m4 * b->m5 + a->m8 * b->m6 + a->m12 * b->m7; r->m5 = a->m1 * b->m4 + a->m5 * b->m5 + a->m9 * b->m6 + a->m13 * b->m7; r->m6 = a->m2 * b->m4 + a->m6 * b->m5 + a->m10* b->m6 + a->m14 * b->m7; r->m7 = a->m3 * b->m4 + a->m7 * b->m5 + a->m11* b->m6 + a->m15 * b->m7; r->m8 = a->m0 * b->m8 + a->m4 * b->m9 + a->m8 * b->m10+ a->m12 * b->m11; r->m9 = a->m1 * b->m8 + a->m5 * b->m9 + a->m9 * b->m10+ a->m13 * b->m11; r->m10= a->m2 * b->m8 + a->m6 * b->m9 + a->m10* b->m10+ a->m14 * b->m11; r->m11= a->m3 * b->m8 + a->m7 * b->m9 + a->m11* b->m10+ a->m15 * b->m11; r->m12= a->m0 * b->m12+ a->m4 * b->m13+ a->m8 * b->m14+ a->m12 * b->m15; r->m13= a->m1 * b->m12+ a->m5 * b->m13+ a->m9 * b->m14+ a->m13 * b->m15; r->m14= a->m2 * b->m12+ a->m6 * b->m13+ a->m10* b->m14+ a->m14 * b->m15; r->m15= a->m3 * b->m12+ a->m7 * b->m13+ a->m11* b->m14+ a->m15 * b->m15; } void MulMatrix4B(Matrix4 *r, Matrix4 *a, Matrix4 *b) { int i, j, k; float sum; for (i=0;i<4;i++) { for (j=0;j<4;j++) { sum = 0.0f; for (k=0;k<4;k++) { sum += ((float *)b)[i*4+k] * ((float *)a)[k*4 + j]; } ((float *)r)[i*4 + j] = sum; } } } 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<1000000; i++) MulMatrix4(&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); printf("start C loop version\n"); gettimeofday(&start, &tzone); for(i=0; i<1000000; i++) MulMatrix4B(&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); printf("start NEON ASM version\n"); gettimeofday(&start, &tzone); for(i=0; i<1000000; i++) Matrix4x4Mul(&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); printf("start NEON optimized ASM version\n"); gettimeofday(&start, &tzone); for(i=0; i<1000000; i++) Matrix4x4Mul2(&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; }
コンパイル
$ as -mcpu=cortex-a7 -mfpu=neon-vfpv4 neonvmul2.s -o neonvmul2.o $ as -mcpu=cortex-a7 -mfpu=neon-vfpv4 neonvmul.s -o neonvmul.o $ gcc -O2 -mfloat-abi=hard -march=armv7-a -mfpu=neon-vfpv4 -o mat4n mat4n.c neonvmul.o neonvmul2.o $ ls -lt -rwxr-xr-x 1 jun jun 7974 Feb 15 14:48 mat4n -rw-r--r-- 1 jun jun 701 Feb 15 14:48 neonvmul.o -rw-r--r-- 1 jun jun 702 Feb 15 14:48 neonvmul2.o -rw-r--r-- 1 jun jun 1142 Feb 15 14:48 neonvmul2.s -rw-r--r-- 1 jun jun 1137 Feb 15 14:48 neonvmul.s -rw-r--r-- 1 jun jun 4319 Feb 15 14:44 mat4n.c
実行
NEON を使って最適化したコードが 0.2 秒で 4x4正方行列の乗算を100万回実行できました。
$ ./mat4n start C version 0.429441 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 start C loop version 1.103722 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 start NEON ASM version 0.233779 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 start NEON optimized ASM version 0.200380 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
【2015/04/13 追記】 CPUクロックを900MHzに固定 した場合は以下のようにさらに速くなります。
$ ./mat4n start C version 0.285946 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 start C loop version 0.735344 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 start NEON ASM version 0.155780 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 start NEON optimized ASM version 0.133519 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
初代 Raspberry Pi で実行してみると、当然ですが「そんな命令は知らない」というエラーになります。
$ ./mat4n $ ./mat4n Illegal instruction
ベンチマークまとめ
前回の各種言語でのベンチマークをRaspberry Pi2でも行ってみました。NEON を使った場合のみ単精度で、その他は倍精度です。 C で書いて -O2 で最適化した場合より 3倍速く(単精度ですが)なりました。 【2015/04/13 追記】 CPUクロックを900MHzに固定 した場合はさらに速くなります。
言語 | 備考 | 実行時間(秒) |
---|---|---|
Java 1.8.0 | - | 1.12 |
Python 2.7.3 | リスト | 86.30 |
NumPy | 21.68 | |
Python 3.2.3 | リスト | 67.25 |
NumPy | 24.10 | |
Lua 5.1.5 | - | 17.04 |
LuaJIT 2.0.0 | テーブル(JIT) | 1.33 |
テーブル(JIT OFF) | 6.39 | |
ffi double (JIT) | 0.93 | |
ffi double (JIT OFF) | 47.51 | |
gcc 4.6.3 | -O0 | 1.76 |
-O2 | 0.61 | |
アセンブリ | ベクトル演算 | 60.81 |
アセンブリ | NEON (最適化後) | 0.20 |
初代 Raspberry Piで行った 前回の結果を再掲します。
言語 | 備考 | 実行時間(秒) |
---|---|---|
Java 1.8.0 | - | 1.60 |
Python 2.7.3 | リスト | 154.06 |
NumPy | 65.80 | |
Python 3.2.3 | リスト | 157.78 |
NumPy | 74.37 | |
Lua 5.1.5 | - | 22.01 |
LuaJIT 2.0.0 | テーブル(JIT) | 2.41 |
テーブル(JIT OFF) | 6.67 | |
ffi double (JIT) | 1.35 | |
ffi double (JIT OFF) | 72.24 | |
gcc 4.6.3 | -O0 | 2.28 |
-O2 | 0.66 | |
アセンブリ | ベクトル演算 | 0.64 |
アセンブリ言語を使って NEON に最適化すると、 Python を使う場合より 400 倍速くなりました。 アセンブラを使って Raspberry Pi2 に最適化したコードを書くと互換性がなくなりますが、クロックあたりの速度は Core2Duo とよく似たレベルになります。 消費電力あたりの速度は圧倒的ですね。「Raspberry Pi は遅い」という意見がありますが、工夫次第ではないでしょうか?