SSEおよびAVX:を介してコピー操作のパフォーマンスを改善しようとしています。
#include <immintrin.h>
const int sz = 1024;
float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
float a=0;
std::generate(mas, mas+sz, [&](){return ++a;});
const int nn = 1000;//Number of iteration in tester loops
std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3;
//std::copy testing
start1 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
std::copy(mas, mas+sz, tar);
end1 = std::chrono::system_clock::now();
float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();
//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=4, _tar+=4)
{
__m128 buffer = _mm_load_ps(_mas);
_mm_store_ps(_tar, buffer);
}
}
end2 = std::chrono::system_clock::now();
float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();
//AVX-copy testing
start3 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m256 buffer = _mm256_load_ps(_mas);
_mm256_store_ps(_tar, buffer);
}
}
end3 = std::chrono::system_clock::now();
float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();
std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"\nSSE gain: "<<elapsed1/elapsed2<<"\nAVX gain: "<<elapsed1/elapsed3;
_mm_free(mas);
_mm_free(tar);
できます。ただし、テスターループの反復回数-nn-は増加しますが、simd-copyのパフォーマンスの向上は減少します。
nn = 10:SSE-ゲイン= 3、AVX-ゲイン= 6;
nn = 100:SSE-ゲイン= 0.75、AVX-ゲイン= 1.5;
nn = 1000:SSE-ゲイン= 0.55、AVX-ゲイン= 1.1;
上記のパフォーマンス低下効果の理由を誰かが説明できますか?コピー操作を手動でベクトル化することをお勧めしますか?
問題は、ベンチマークを困難にするハードウェアのいくつかの要因を移行するために、テストがうまく機能しないことです。これをテストするために、私は独自のテストケースを作成しました。このようなもの:
for blah blah:
sleep(500ms)
std::copy
sse
axv
出力:
SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy
したがって、この場合、AVXはstd::copy
よりも高速です。テストケースをに変更するとどうなりますか。
for blah blah:
sleep(500ms)
sse
axv
std::copy
テストの順序を除いて、まったく何も変わっていないことに注意してください。
SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy
うわー!そんなことがあるものか? CPUがフルスピードに達するまでには時間がかかるため、後で実行されるテストには利点があります。この質問には、「承認済み」の回答を含む3つの回答があります。しかし、賛成票の数が最も少ないものだけが正しい方向に進んでいました。
これがベンチマークが難しい理由の1つであり、セットアップの詳細情報が含まれていない限り、誰かのマイクロベンチマークを信頼してはいけません。うまくいかないのはコードだけではありません。省電力機能と奇妙なドライバーは、ベンチマークを完全に台無しにする可能性があります。あるとき、ノートブックの1%未満が提供するBIOSのスイッチを切り替えることで、パフォーマンスの7倍の違いを測定しました。
これは非常に興味深い質問ですが、質問自体が非常に誤解を招くため、これまでの回答はどれも正しくないと思います。
タイトルを「理論上のメモリI/O帯域幅に到達するにはどうすればよいですか?」に変更する必要があります。
どの命令セットを使用しても、CPUはRAMよりもはるかに高速であるため、純粋なブロックメモリコピーは100%I/Oに制限されます。そしてこれは、SSEとAVXのパフォーマンスにほとんど違いがない理由を説明しています。
L1Dキャッシュでホットな小さなバッファの場合、AVXは、256bロード/ストアが2つの128b操作に分割する代わりにL1Dキャッシュへの256bデータパスを実際に使用するHaswellのようなCPUでSSEよりも大幅に高速にコピーできます。
皮肉なことに、古代のX86命令rep stosqは、SSEおよびAVXよりもはるかに優れたパフォーマンスを発揮しますメモリコピーの観点から!
ここの記事 メモリ帯域幅を本当にうまく飽和させる方法を説明し、さらに探求するための豊富なリファレンスもあります。
参照 memcpyの拡張REP MOVSB ここSOで、@ BeeOnRopeの回答では、NTストア(およびrep stosb/stosq
によって実行される非RFOストア)と通常のストア、およびシングルコアメモリについて説明しています。多くの場合、帯域幅は、メモリコントローラ自体ではなく、最大同時実行性/遅延によって制限されます。
高速な書き込みSSEは、同等の非並列演算の代わりにSSE演算を使用するほど簡単ではありません。この場合、コンパイラは負荷を有効に展開できないと思われます/ storeペアとあなたの時間は、次の命令(ストア)で1つの低スループット操作(ロード)の出力を使用することによって引き起こされるストールによって支配されます。
このアイデアは、1つのノッチを手動で展開することでテストできます。
//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m128 buffer1 = _mm_load_ps(_mas);
__m128 buffer2 = _mm_load_ps(_mas+4);
_mm_store_ps(_tar, buffer1);
_mm_store_ps(_tar+4, buffer2);
}
}
通常、組み込み関数を使用するときは、出力を逆アセンブルして、何もおかしなことが起こっていないことを確認します(これを試して、元のループが展開されたかどうか/どのように展開されたかを確認できます)。より複雑なループの場合、使用する適切なツールは Intel Architecture Code Analyzer(IACA) です。これは、「パイプラインストールがあります」などの情報を提供できる静的分析ツールです。
これは、ちょっと短い操作では測定が正確ではないためだと思います。
IntelCPUでパフォーマンスを測定する場合
「ターボブースト」と「SpeedStep」を無効にします。これは、システムBIOSで実行できます。
プロセス/スレッドの優先度を高またはリアルタイムに変更します。これにより、スレッドが実行され続けます。
プロセスCPUマスクを1つのコアのみに設定します。優先度の高いCPUマスキングは、コンテキストの切り替えを最小限に抑えます。
__rdtsc()組み込み関数を使用します。 Intel Coreシリーズは、__ rdtsc()を使用してCPU内部クロックカウンターを返します。 3.4GhzCPUから3400000000カウント/秒を取得します。また、__ rdtsc()は、CPUでスケジュールされたすべての操作をフラッシュするため、タイミングをより正確に測定できます。
これは、SSE/AVXコードをテストするためのテストベッド起動コードです。
int GetMSB(DWORD_PTR dwordPtr)
{
if(dwordPtr)
{
int result = 1;
#if defined(_WIN64)
if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8; dwordPtr &= 0xFF00FF00FF00FF00; }
if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4; dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2; dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
#else
if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
if(dwordPtr & 0xFF00FF00) { result += 8; dwordPtr &= 0xFF00FF00; }
if(dwordPtr & 0xF0F0F0F0) { result += 4; dwordPtr &= 0xF0F0F0F0; }
if(dwordPtr & 0xCCCCCCCC) { result += 2; dwordPtr &= 0xCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAA) { result += 1; }
#endif
return result;
}
else
{
return 0;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
// Set Core Affinity
DWORD_PTR processMask, systemMask;
GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );
// Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
DWORD64 start, end;
start = __rdtsc();
// your code here.
end = __rdtsc();
printf("%I64d\n", end - start);
return 0;
}
あなたの主な問題/ボトルネックはあなたの _mm_malloc
だと思います。
C++の局所性が心配な場合は、メインのデータ構造としてstd::vector
を使用することを強くお勧めします。
組み込み関数は、正確には「ライブラリ」ではなく、builtinに似ています。 )コンパイラから提供される関数。この関数を使用する前に、コンパイラの内部/ドキュメントに精通している必要があります。
また、AVX
がSSE
よりも新しいという事実は、使用する予定が何であれ、AVX
を高速化するわけではないことに注意してください。関数はおそらく「avxvssse」引数よりも重要です。たとえば、 この回答 を参照してください。
POD int array[]
またはstd::vector
で試してください。