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 は遅い」という意見がありますが、工夫次第ではないでしょうか?



続く...



このページの目次