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

SSE の浮動小数演算速度

Cortex-A8 の NEON のように SSE も試してみました。
やりかたは前回と全く同じように、ひたすら命令を並べてスループットの測定。
今回はスレッドにも対応しておきます。

static void Thread_SSE_MULPS( Thread::Sync* barrier )
{
    __asm {
        xorps   xmm0, xmm0
        ~
    };
    barrier->Barrier( THREAD_COUNT );
    for( int i= 0 ; i< VECTOR_LOOP ; i++ ){
        __asm {
            mulps   xmm0, xmm1
            mulps   xmm1, xmm2
            mulps   xmm2, xmm3
            mulps   xmm3, xmm4
            mulps   xmm4, xmm5
            mulps   xmm5, xmm6
            mulps   xmm6, xmm7
            mulps   xmm7, xmm0
		~
	};
    }
    barrier->Barrier( THREAD_COUNT );
}

void Start_SSE( void (*func)( Thread::Sync* ), const char* msg )
{
    MultiThreadClass    thread;
    Thread::Sync barrier;
    barrier.Init();

    thread.Start( func, &barrier );

    TimerClass  timer;
    timer.Begin();

    func( &barrier );

    timer.End( msg );

    barrier.Quit();
    thread.Join();
}

void SSE_main()
{
    Start_SSE( Thread_SSE_MULPS, "SSE_MULPS" );
}

上記のようにメモリアクセスの無い命令を前回同様に 40 個 (8×5) 並べます。

スレッドの開始と終了待ちはシグナルで同期します。一番遅いスレッドの終了に合わせているため、
速く終了したスレッドがあると若干無駄が生じる可能性があります。
メインスレッドが実行している間に、サブスレッドで何回命令を実行出来たか数えた方が
より正確な結果になるかもしれません。

Core i7 920 2.67GHz を使っています。1 スレッドあたり 1G 回ループします。

SSE_MULPS  14.32 sec  Thread x1

40命令 × 1G 回ループなので 40/14.32 = 2.79 。
CPU は 2.67GHz なので 1命令 / 1cycle に近い値だけど少々オーバーしています。
ターボブーストのおかげでしょうか。スレッドを増やしてみます。

SSE_MULPS  16.19 sec  Thread x4
SSE_MULPS  29.05 sec  Thread x8

core 数 (4) までは大きく増えませんが、8スレッド時は、1スレッドのほぼ 2倍の時間が
かかっています。HT を含めるとハードウエアで 8スレッドですが、パイプラインに空きが
ないためか HT の効果が全く無いことがわかります。
8スレッド時の値を元に計算してみると Core i7 920 2.67GHz は mulps で

40命令 × 1G回ループ × 4 SIMD × 8 Thread / 29.05 秒 = およそ 44.06 G FLOPS

SSE には積和がありませんが SSE4.1 には DPPS があることを思い出しました。
Mul x4 + Add x3 なので試してみます。

        __asm {
            dpps    xmm0, xmm1, 255
            dpps    xmm1, xmm2, 255
            dpps    xmm2, xmm3, 255
                ~
SSE_DPPS  28.74 sec  Thread x1
SSE_DPPS  31.02 sec  Thread x4
SSE_DPPS  57.73 sec  Thread x8

mul のちょうど 2倍、スループット 2 で実行しているようです。
2 cycle で 7 float なので演算量自体は mulps に劣ります。

最初の乗算を敢えてストールするように組み替えてみます。

        __asm {
            mulps   xmm0, xmm0
            mulps   xmm0, xmm0
            mulps   xmm0, xmm0
                ~
SSE_MULPS_ST  57.36 sec  Thread x1
SSE_MULPS_ST  57.51 sec  Thread x4
SSE_MULPS_ST  58.63 sec  Thread x8

インターリーブ時は 14.32秒だったので、4倍の実行時間がかかるようになりました。
mulps のレイテンシが 4 cycle あることがわかります。パイプラインはがら空きで、
4スレッド時と 8スレッド時の実行速度に差がありません。HT がめいっぱい働いており、
それでもまだ半分ほど無駄が生じていることになります。

SSE_DPPS_ST  157.57 sec  Thread x1
SSE_DPPS_ST  157.68 sec  Thread x4
SSE_DPPS_ST  157.74 sec  Thread x8

同じように dpps もストールさせてみると、こちらはさらに時間がかかります。
5.5 倍かかっているためスループット 2 、レイテンシ 11 で実行しているようです。

Atom でも試してみました。遅いのでループ回数を 1/10 (100M) に減らしています。

Atom 1.8GHz  SSE_MULPS      4.36 sec  Thread x1
Atom 1.8GHz  SSE_MULPS      8.70 sec  Thread x2
Atom 1.8GHz  SSE_ADDPS      2.25 sec  Thread x1
Atom 1.8GHz  SSE_ADDPS      4.46 sec  Thread x1
Atom 800MHz  SSE_MULPS     10.15 sec  Thread x1
Atom 800MHz  SSE_MULPS     20.29 sec  Thread x2

乗算は 2 float/cycle、加算で 4 float/cycle 実行しています。
やはりパイプラインを埋めると HT の効果が無いことがわかります。

Atom 1.8GHz  SSE_MULPS_ST  10.88 sec  Thread x1
Atom 1.8GHz  SSE_MULPS_ST  10.92 sec  Thread x2
Atom 1.8GHz  SSE_MULPS_ST  21.92 sec  Thread x4

乗算はストール発生時 2.5 倍遅くなっています。スループット 2/ レイテンシ 5

結果として Atom も Cortex-A8 + NEON も、1 cycle あたり 2乗算 or 4加算で同等です で同等、加算は Atom の方が高速です。
ただし積和の場合は Atom は 3cycle かかるところ NEON は 2cycle で済むため、
この組み合わせでは Cortex-A8 + NEON の方が速いことになります。
浮動小数演算も同クロック数の Atom より高速に実行できそうです。

2009/10/02 追記>: 加算が多いと Atom の方が速く、最適化が弱い場合は HT が有効な分 Atom が有利なこともあります。

もちろん他の命令の実行速度にもよるし、メモリアクセスや演算以外の転送命令等も絡んで
くるので一概には言えません。レイテンシも大きめなので最適化をどれだけ出来るかに
かかってきます。

厳密なデータではありませんが、以前の結果を見ても浮動小数演算以外は同サイクルの
Atom より速い速度で実行できているし、浮動小数演算も潜在能力は高そうです。
問題は VFP でしょうか。

関連エントリ
NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
NetWalker PC-Z1 Atom と速度比較