3

以下のコードでは、「dataLen」を変更して効率を変えています。

dataLen = 400 SSE 時間:758000 us AVX 時間:483000 us SSE > AVX

dataLen = 2400 SSE 時間:4212000 us AVX 時間:2636000 us SSE > AVX

dataLen = 2864 SSE 時間:6115000 us AVX 時間:6146000 us SSE ~= AVX

dataLen = 3200 SSE 時間:8049000 us AVX 時間:9297000 us SSE < AVX

dataLen = 4000 SSE 時間:10170000us AVX 時間:11690000us SSE < AVX

SSE と AVX のコードは、どちらも次のように簡略化できます。 buf3[i] += buf1[1]*buf2[i];

#include "testfun.h"
#include <iostream>
#include <chrono>
#include <malloc.h>
#include "immintrin.h"
using namespace std::chrono;

void testfun()
{
int dataLen = 4000; 
int N = 10000000;
float *buf1 = reinterpret_cast<float*>(_aligned_malloc(sizeof(float)*dataLen, 32));
float *buf2 = reinterpret_cast<float*>(_aligned_malloc(sizeof(float)*dataLen, 32));
float *buf3 = reinterpret_cast<float*>(_aligned_malloc(sizeof(float)*dataLen, 32));
for(int i=0; i<dataLen; i++)
{
    buf1[i] = 1;
    buf2[i] = 1;
    buf3[i] = 0;
}
//=========================SSE CODE=====================================
system_clock::time_point SSEStart = system_clock::now();
__m128 p1, p2, p3;

for(int j=0; j<N; j++)
for(int i=0; i<dataLen; i=i+4)
{
    p1 = _mm_load_ps(&buf1[i]);
    p2 = _mm_load_ps(&buf2[i]);
    p3 = _mm_load_ps(&buf3[i]);
    p3 = _mm_add_ps(_mm_mul_ps(p1, p2), p3);
    _mm_store_ps(&buf3[i], p3);
}

microseconds SSEtimeUsed = duration_cast<milliseconds>(system_clock::now() - SSEStart);
std::cout << "SSE time used: " << SSEtimeUsed.count() << " us, " <<std::endl;

//=========================AVX CODE=====================================
for(int i=0; i<dataLen; i++) buf3[i] = 0;

system_clock::time_point AVXstart = system_clock::now();
__m256  pp1, pp2, pp3; 

for(int j=0; j<N; j++)
for(int i=0; i<dataLen; i=i+8)
{       
    pp1 = _mm256_load_ps(&buf1[i]);
    pp2 = _mm256_load_ps(&buf2[i]);
    pp3 = _mm256_load_ps(&buf3[i]);
    pp3 = _mm256_add_ps(_mm256_mul_ps(pp1, pp2), pp3);
    _mm256_store_ps(&buf3[i], pp3);

}

microseconds AVXtimeUsed = duration_cast<milliseconds>(system_clock::now() - AVXstart);
std::cout << "AVX time used: " << AVXtimeUsed.count() << " us, " <<std::endl;

_aligned_free(buf1);
_aligned_free(buf2);
}

私の CPU は Intel Xeon E3-1225 v2 で、L1 キャッシュは 32KB*4 (4 コア) です。このコードを実行すると、1 つのコアしか使用しないため、使用される L1 キャッシュは 32KB です。

buf1 buf2 と buf3 は L1 キャッシュと L2 キャッシュ (L2 キャッシュ 1MB) に配置するのに十分小さいです。

4

2 に答える 2

3

それは興味深い観察です。私はあなたの結果を再現することができました。ループを展開することで、SSE コードの速度を大幅に改善することができました (以下のコードを参照)。現在、SSEdataLen=2864は明らかに高速であり、値が小さい場合は AVX とほぼ同じ速さです。値が大きいほど、さらに高速になります。これは、SSE コードのキャリー ループ依存性によるものです (つまり、ループをアンロールすると、命令レベルの並列処理 (ILP) が増加します)。それ以上展開しようとはしませんでした。AVX コードを展開しても役に立ちませんでした。

私はあなたの質問に対する明確な答えを持っていません。私の推測では、これは ILP と、Sandy Bridge などの AVX プロセッサが同時に 2 つの 128 ビット ワード (SSE 幅) しかロードできず、2 つの 256 ビット ワードをロードできないという事実に関連しているということです。したがって、SSE コードでは、1 つの SSE 加算、1 つの SSE 乗算、2 つの SSE ロード、および 1 つの SSE ストアを同時に実行できます。AVX の場合、1 つの AVX ロード (ポート 2 および 3 での 2 つの 128 ビット ロードによる)、1 つの AVX 乗算、1 つの AVX 加算、および 1 つの 128 ビット ストア (AVX 幅の半分) を同時に実行できます。つまり、AVX では乗算と加算が SSE の 2 倍の作業を行いますが、ロードとストアは依然として 128 ビット幅です。おそらくこれは、コードがロードとストアによって支配される場合がある SSE と比較して、AVX での ILP の低下につながるのでしょうか?

ポートと ILP の詳細については、このHaswell、Sandy Bridge、Nehalem ポートの比較を参照してください。

__m128 p1, p2, p3, p1_v2, p2_v2, p3_v2;
for(int j=0; j<N; j++)
    for(int i=0; i<dataLen; i+=8)
    {
        p1 = _mm_load_ps(&buf1[i]);
        p1_v2 = _mm_load_ps(&buf1[i+4]);
        p2 = _mm_load_ps(&buf2[i]);
        p2_v2 = _mm_load_ps(&buf2[i+4]);
        p3 = _mm_load_ps(&buf3[i]);
        p3_v2 = _mm_load_ps(&buf3[i+4]);
        p3 = _mm_add_ps(_mm_mul_ps(p1, p2), p3);
        p3_v2 = _mm_add_ps(_mm_mul_ps(p1_v2, p2_v2), p3_v2);
        _mm_store_ps(&buf3[i], p3);
        _mm_store_ps(&buf3[i+4], p3_v2);
    }
于 2013-09-09T11:37:34.857 に答える