最善ケースばかりでなく、実際に使えるプログラムで試してみます。
使っているのは i.MX51 系 i.MX515 を搭載した NetWalker PC-Z1 ですが、
おそらく同じ Cortex-A8 (ARM v7-A) の CPU core を使った iPhone 3GS や
iPod touch 3G でも全く同様ではないかと思います。
NEON のプログラミングは比較的簡単です。
例えば 4×4 matrix の乗算だとこんな感じで書けます。
inline void Mul_NEON( Float4x4* p3, const Float4x4* p1, const Float4x4* p2 ) { __asm__ __volatile__ ( "\ vldmia %0, {d0-d7} \n\ vldmia %1, {d8-d15} \n\ \n\ vmul.f32 q8,q0,d8[0] \n\ vmla.f32 q8,q1,d8[1] \n\ vmla.f32 q8,q2,d9[0] \n\ vmla.f32 q8,q3,d9[1] \n\ vstmia %2!, {d16,d17} \n\ \n\ vmul.f32 q8,q0,d10[0] \n\ vmla.f32 q8,q1,d10[1] \n\ vmla.f32 q8,q2,d11[0] \n\ vmla.f32 q8,q3,d11[1] \n\ vstmia %2!, {d16,d17} \n\ \n\ vmul.f32 q8,q0,d12[0] \n\ vmla.f32 q8,q1,d12[1] \n\ vmla.f32 q8,q2,d13[0] \n\ vmla.f32 q8,q3,d13[1] \n\ vstmia %2!, {d16,d17} \n\ \n\ vmul.f32 q8,q0,d14[0] \n\ vmla.f32 q8,q1,d14[1] \n\ vmla.f32 q8,q2,d15[0] \n\ vmla.f32 q8,q3,d15[1] \n\ vstmia %2!, {d16,d17} \n\ " : "=&r"( p1 ), "=&r"( p2 ), "=&r"( p3 ) : "0"( p1 ), "1"( p2 ), "2"( p3 ) : "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "cc", "memory" ); }
かなり少ない命令で記述できます。
・3オペランドの命令フォーマットでレジスタの転送が不要
・積和命令がある
・ベクタへのスカラ乗算機能が用意されているため、スカラ要素の複製が不要
・レジスタの数が多い
d0~d31 : 64bit レジスタ (float x2) 32本 q1~q16 : 128bit レジスタ (float x4) 16本 (d0~d31 と共有)
上のプログラムで配列のような記述をしている d8[0], d8[1] は、d0レジスタに含まれる
2つの float に個別にアクセスしています。
s0~s31 は 32bit の single float レジスタですが、この形式で記述する命令は
VFP 命令なので要注意です。NEON View では d または q レジスタのみ扱い、
single 要素は d レジスタの一部として扱います。
なお VFP 命令も倍精度演算で d レジスタを扱うことがあります。
vmla は積和命令で vmla.f32 q8,q0,d8[1] はシェーダー風の記述を用いるなら
q8.xyzw = q0.xyzw * d8.xxxx + q8.xyzw
となります。ベクタへのスカラ乗算を簡単に記述できるのは便利です。
SSE だと 3~4命令くらいかかります。
これが SSE なら
void __fastcall Mul_SSE( Float4x4* p3, const Float4x4* p1, const Float4x4* p2 ) { __asm { mov eax, dword ptr[esp+4] ; p2 movaps xmm4, xmmword ptr[edx] ; p1->_11_12_13_14 movss xmm0, xmmword ptr[eax] ; p2->_11 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movaps xmm5, xmmword ptr[edx+16] ; p1->_21_22_23_24 movss xmm0, xmmword ptr[eax+4] ; p2->_12 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movaps xmm6, xmmword ptr[edx+32] ; p1->_31_32_33_34 movss xmm0, xmmword ptr[eax+8] ; p2->_13 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movaps xmm7, xmmword ptr[edx+48] ; p1->_41_42_43_44 movss xmm0, xmmword ptr[eax+12] ; p2->_14 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx], xmm1 ; p3->_11_12_13_14 movss xmm0, xmmword ptr[eax+16] ; p2->_21 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movss xmm0, xmmword ptr[eax+20] ; p2->_22 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+24] ; p2->_23 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+28] ; p2->_24 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx+16], xmm1 ; p3->_21_22_23_24 movss xmm0, xmmword ptr[eax+32] ; p2->_31 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movss xmm0, xmmword ptr[eax+36] ; p2->_32 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+40] ; p2->_33 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+44] ; p2->_34 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx+32], xmm1 ; p3->_31_32_33_34 movss xmm0, xmmword ptr[eax+48] ; p2->_41 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movss xmm0, xmmword ptr[eax+52] ; p2->_42 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+56] ; p2->_43 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+60] ; p2->_44 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx+48], xmm1 ; p3->_41_42_43_44 }; }
長いです。
その代わり Cortex-A8 / NEON はインオーダー実行 HT 無しなので、最適化を考えて
書く必要があります。
最初の NEON の例も、レジスタが多いことを利用すれば次のようにできます。
vldmia %0, {d0-d7} vldmia %1, {d8-d15} vmul.f32 q8,q0,d8[0] vmul.f32 q9,q0,d10[0] vmul.f32 q10,q0,d12[0] vmul.f32 q11,q0,d14[0] vmla.f32 q8,q1,d8[1] vmla.f32 q9,q1,d10[1] vmla.f32 q10,q1,d12[1] vmla.f32 q11,q1,d14[1] vmla.f32 q8,q2,d9[0] vmla.f32 q9,q2,d11[0] vmla.f32 q10,q2,d13[0] vmla.f32 q11,q2,d15[0] vmla.f32 q8,q3,d9[1] vmla.f32 q9,q3,d11[1] vmla.f32 q10,q3,d13[1] vmla.f32 q11,q3,d15[1] vstmia %2, {d16-d23}
完全にキャッシュがヒットする前提なら、こちらのコードの方が 1.7倍くらい速くなります。
ただし、実際のアプリケーションで使うとここまで差が出ません。
メモリアクセスの方がずっと低速だからです。
キャッシュがほとんど利かない条件でテストすると、Cortex-A8 + NEON は Atom よりも
かなり低速でした。Matrix 演算ではなく、ただのメモリ転送だけテストしてみたのが下の表です。
Atom Z540 1.86GHz 2.98GB/sec Atom Z540 800MHz 1.91GB/sec (省電力機能で制限をかけたもの) Cortex-A8 800MHz 255MB/sec
Atom Z540 は FSB 533MHz で 64bit 幅あるため、DDR2-533 とするとメモリの転送速度は
最大 4.2GB/sec (PC2-4200) と考えられます。
i.MX515 は mDDR1 or DDR2 200MHz の 16/32bit らしいですが、この辺の具体的な値は
明らかになっていません。ただ考えられる数値よりも測定結果はかなり低いです。
省電力との兼ね合いでしょうか。L2 cache 容量も Atom の半分です。
プロセッサ自体の演算能力はそれなりにあるものの、メモリ上のデータを大量にストリーム処理
するようなケースでは、ほとんど生かし切れていない可能性があります。
同様の core を持つ iPhone 3GS でどの程度動くか試してみたいところです。
関連エントリ
・SSE の浮動小数演算速度
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較