日別アーカイブ: 2009年10月3日

NetWalker PC-Z1 Cortex-A8 の NEON 命令とメモリ速度

最善ケースばかりでなく、実際に使えるプログラムで試してみます。
使っているのは 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 と速度比較